Fixed an issue in idmapper metaclass wrapper, it caused text fields to be occationally be written with objects.
This commit is contained in:
parent
bbba695380
commit
0a394929b7
7 changed files with 146 additions and 45 deletions
|
|
@ -214,6 +214,7 @@ def format_script_list(scripts):
|
||||||
table.align = 'r'
|
table.align = 'r'
|
||||||
for script in scripts:
|
for script in scripts:
|
||||||
nextrep = script.time_until_next_repeat()
|
nextrep = script.time_until_next_repeat()
|
||||||
|
print "@script:", script.key, type(script.key)
|
||||||
table.add_row([script.id,
|
table.add_row([script.id,
|
||||||
script.obj.key if (hasattr(script, 'obj') and script.obj) else "<Global>",
|
script.obj.key if (hasattr(script, 'obj') and script.obj) else "<Global>",
|
||||||
script.key,
|
script.key,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,11 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE)
|
||||||
# Command testing
|
# Command testing
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
def dummy_data_out(self, text=None, **kwargs):
|
def dummy(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
SESSIONS.data_out = dummy_data_out
|
|
||||||
|
SESSIONS.data_out = dummy
|
||||||
|
SESSIONS.disconnect = dummy
|
||||||
|
|
||||||
class TestObjectClass(Object):
|
class TestObjectClass(Object):
|
||||||
def msg(self, text="", **kwargs):
|
def msg(self, text="", **kwargs):
|
||||||
|
|
@ -46,6 +48,10 @@ class TestCharacterClass(Character):
|
||||||
"test message"
|
"test message"
|
||||||
if self.player:
|
if self.player:
|
||||||
self.player.msg(text=text, **kwargs)
|
self.player.msg(text=text, **kwargs)
|
||||||
|
else:
|
||||||
|
if not self.ndb.stored_msg:
|
||||||
|
self.ndb.stored_msg = []
|
||||||
|
self.ndb.stored_msg.append(text)
|
||||||
class TestPlayerClass(Player):
|
class TestPlayerClass(Player):
|
||||||
def msg(self, text="", **kwargs):
|
def msg(self, text="", **kwargs):
|
||||||
"test message"
|
"test message"
|
||||||
|
|
@ -116,7 +122,8 @@ class CommandTest(TestCase):
|
||||||
cmdobj.func()
|
cmdobj.func()
|
||||||
cmdobj.at_post_cmd()
|
cmdobj.at_post_cmd()
|
||||||
# clean out prettytable sugar
|
# clean out prettytable sugar
|
||||||
returned_msg = "|".join(_RE.sub("", mess) for mess in self.char1.player.ndb.stored_msg)
|
stored_msg = self.char1.player.ndb.stored_msg if self.char1.player else self.char1.ndb.stored_msg
|
||||||
|
returned_msg = "|".join(_RE.sub("", mess) for mess in stored_msg)
|
||||||
#returned_msg = "|".join(self.char1.player.ndb.stored_msg)
|
#returned_msg = "|".join(self.char1.player.ndb.stored_msg)
|
||||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||||
if msg != None:
|
if msg != None:
|
||||||
|
|
@ -189,6 +196,8 @@ class TestPlayer(CommandTest):
|
||||||
CID = 5
|
CID = 5
|
||||||
def test_cmds(self):
|
def test_cmds(self):
|
||||||
self.call(player.CmdOOCLook(), "", "Account TestPlayer5 (you are OutofCharacter)", caller=self.player)
|
self.call(player.CmdOOCLook(), "", "Account TestPlayer5 (you are OutofCharacter)", caller=self.player)
|
||||||
|
self.call(player.CmdOOC(), "", "You are already", caller=self.player)
|
||||||
|
self.call(player.CmdIC(), "Char5","You become Char5.", caller=self.player)
|
||||||
self.call(player.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.player)
|
self.call(player.CmdPassword(), "testpassword = testpassword", "Password changed.", caller=self.player)
|
||||||
self.call(player.CmdEncoding(), "", "Default encoding:", caller=self.player)
|
self.call(player.CmdEncoding(), "", "Default encoding:", caller=self.player)
|
||||||
self.call(player.CmdWho(), "", "Players:", caller=self.player)
|
self.call(player.CmdWho(), "", "Players:", caller=self.player)
|
||||||
|
|
@ -197,8 +206,6 @@ class TestPlayer(CommandTest):
|
||||||
self.call(player.CmdColorTest(), "ansi", "ANSI colors:", caller=self.player)
|
self.call(player.CmdColorTest(), "ansi", "ANSI colors:", caller=self.player)
|
||||||
self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player)
|
self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player)
|
||||||
self.call(player.CmdQuell(), "", "Quelling Player permissions (immortals). Use @unquell to get them back.", caller=self.player)
|
self.call(player.CmdQuell(), "", "Quelling Player permissions (immortals). Use @unquell to get them back.", caller=self.player)
|
||||||
self.call(player.CmdIC(), "Char5","Char5 is now acted from another", caller=self.player)
|
|
||||||
self.call(player.CmdOOC(), "", "You are already", caller=self.player)
|
|
||||||
|
|
||||||
from src.commands.default import building
|
from src.commands.default import building
|
||||||
class TestBuilding(CommandTest):
|
class TestBuilding(CommandTest):
|
||||||
|
|
|
||||||
|
|
@ -523,8 +523,6 @@ class Channel(SharedMemoryModel):
|
||||||
logger.log_errmsg("Lock_Storage (on %s) cannot be deleted. Use obj.lock.delete() instead." % self)
|
logger.log_errmsg("Lock_Storage (on %s) cannot be deleted. Use obj.lock.delete() instead." % self)
|
||||||
lock_storage = property(lock_storage_get, lock_storage_set, lock_storage_del)
|
lock_storage = property(lock_storage_get, lock_storage_set, lock_storage_del)
|
||||||
|
|
||||||
db_model_name = "channel" # used by attributes to safely store objects
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"Define Django meta options"
|
"Define Django meta options"
|
||||||
verbose_name = "Channel"
|
verbose_name = "Channel"
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,8 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg
|
||||||
# try to see if there is a handler on object that should be triggered when saving.
|
# try to see if there is a handler on object that should be triggered when saving.
|
||||||
handlername = "_at_%s_save" % fieldname
|
handlername = "_at_%s_save" % fieldname
|
||||||
handler = _GA(instance, handlername) if handlername in _GA(instance, '__dict__') else None
|
handler = _GA(instance, handlername) if handlername in _GA(instance, '__dict__') else None
|
||||||
|
#if handlername == "_at_db_location_save":
|
||||||
|
# print "handler:", handlername, handler, _GA(sender, '__dict__').keys()
|
||||||
if callable(handler):
|
if callable(handler):
|
||||||
#hid = hashid(instance, "-%s" % fieldname)
|
#hid = hashid(instance, "-%s" % fieldname)
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,8 @@ class TypeClass(object):
|
||||||
dbobj.dbid,
|
dbobj.dbid,
|
||||||
dbobj.typeclass_path,))
|
dbobj.typeclass_path,))
|
||||||
|
|
||||||
def __str__(self):
|
# def __str__(self):
|
||||||
"represent the object"
|
# "represent the object"
|
||||||
return self.key
|
# return _GA(self, "key")
|
||||||
def __unicode__(self):
|
# def __unicode__(self):
|
||||||
return u"%s" % self.key
|
# return u"%s" % _GA(self, "key")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from twisted.internet.reactor import callFromThread
|
||||||
from django.core.exceptions import ObjectDoesNotExist, FieldError
|
from django.core.exceptions import ObjectDoesNotExist, FieldError
|
||||||
from django.db.models.base import Model, ModelBase
|
from django.db.models.base import Model, ModelBase
|
||||||
from django.db.models.signals import post_save, pre_delete, post_syncdb
|
from django.db.models.signals import post_save, pre_delete, post_syncdb
|
||||||
from src.utils.utils import dbref, get_evennia_pids
|
from src.utils.utils import dbref, get_evennia_pids, to_str
|
||||||
|
|
||||||
from manager import SharedMemoryManager
|
from manager import SharedMemoryManager
|
||||||
|
|
||||||
|
|
@ -30,7 +30,6 @@ _DA = object.__delattr__
|
||||||
from src import PROC_MODIFIED_OBJS
|
from src import PROC_MODIFIED_OBJS
|
||||||
|
|
||||||
# get info about the current process and thread
|
# get info about the current process and thread
|
||||||
|
|
||||||
_SELF_PID = os.getpid()
|
_SELF_PID = os.getpid()
|
||||||
_SERVER_PID, _PORTAL_PID = get_evennia_pids()
|
_SERVER_PID, _PORTAL_PID = get_evennia_pids()
|
||||||
_IS_SUBPROCESS = (_SERVER_PID and _PORTAL_PID) and not _SELF_PID in (_SERVER_PID, _PORTAL_PID)
|
_IS_SUBPROCESS = (_SERVER_PID and _PORTAL_PID) and not _SELF_PID in (_SERVER_PID, _PORTAL_PID)
|
||||||
|
|
@ -74,7 +73,7 @@ class SharedMemoryModelBase(ModelBase):
|
||||||
cls.__instance_cache__ = {} #WeakValueDictionary()
|
cls.__instance_cache__ = {} #WeakValueDictionary()
|
||||||
super(SharedMemoryModelBase, cls)._prepare()
|
super(SharedMemoryModelBase, cls)._prepare()
|
||||||
|
|
||||||
def __init__(cls, *args, **kwargs):
|
def __new__(cls, classname, bases, classdict, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Field shortcut creation:
|
Field shortcut creation:
|
||||||
Takes field names db_* and creates property wrappers named without the db_ prefix. So db_key -> key
|
Takes field names db_* and creates property wrappers named without the db_ prefix. So db_key -> key
|
||||||
|
|
@ -82,15 +81,15 @@ class SharedMemoryModelBase(ModelBase):
|
||||||
already has a wrapper of the given name, the automatic creation is skipped. Note: Remember to
|
already has a wrapper of the given name, the automatic creation is skipped. Note: Remember to
|
||||||
document this auto-wrapping in the class header, this could seem very much like magic to the user otherwise.
|
document this auto-wrapping in the class header, this could seem very much like magic to the user otherwise.
|
||||||
"""
|
"""
|
||||||
super(SharedMemoryModelBase, cls).__init__(*args, **kwargs)
|
def create_wrapper(cls, fieldname, wrappername, editable=True, foreignkey=False):
|
||||||
def create_wrapper(cls, fieldname, wrappername, editable=True):
|
|
||||||
"Helper method to create property wrappers with unique names (must be in separate call)"
|
"Helper method to create property wrappers with unique names (must be in separate call)"
|
||||||
def _get(cls, fname):
|
def _get(cls, fname):
|
||||||
"Wrapper for getting database field"
|
"Wrapper for getting database field"
|
||||||
value = _GA(cls, fieldname)
|
value = _GA(cls, fieldname)
|
||||||
if type(value) in (basestring, int, float, bool):
|
if isinstance(value, (basestring, int, float, bool)):
|
||||||
return value
|
return value
|
||||||
elif hasattr(value, "typeclass"):
|
elif hasattr(value, "typeclass"):
|
||||||
|
if fieldname == "db_key": print "idmapper _get typeclass:, ", cls.__class__.__name__, fieldname, _GA(value, "typeclass")
|
||||||
return _GA(value, "typeclass")
|
return _GA(value, "typeclass")
|
||||||
return value
|
return value
|
||||||
def _set_nonedit(cls, fname, value):
|
def _set_nonedit(cls, fname, value):
|
||||||
|
|
@ -98,22 +97,32 @@ class SharedMemoryModelBase(ModelBase):
|
||||||
raise FieldError("Field %s cannot be edited." % fname)
|
raise FieldError("Field %s cannot be edited." % fname)
|
||||||
def _set(cls, fname, value):
|
def _set(cls, fname, value):
|
||||||
"Wrapper for setting database field"
|
"Wrapper for setting database field"
|
||||||
#print "_set:", fname
|
if fname=="db_key": print "db_key _set:", value, type(value)
|
||||||
if hasattr(value, "dbobj"):
|
_SA(cls, fname, value)
|
||||||
|
# only use explicit update_fields in save if we actually have a
|
||||||
|
# primary key assigned already (won't be set when first creating object)
|
||||||
|
update_fields = [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
|
||||||
|
_GA(cls, "save")(update_fields=update_fields)
|
||||||
|
def _set_foreign(cls, fname, value):
|
||||||
|
"Setter only used on foreign key relations, allows setting with #dbref"
|
||||||
|
try:
|
||||||
value = _GA(value, "dbobj")
|
value = _GA(value, "dbobj")
|
||||||
elif isinstance(value, basestring) and (value.isdigit() or value.startswith("#")):
|
except AttributeError:
|
||||||
# we also allow setting using dbrefs, if so we try to load the matching object.
|
pass
|
||||||
# (we assume the object is of the same type as the class holding the field, if
|
if isinstance(value, (basestring, int)):
|
||||||
# not a custom handler must be used for that field)
|
value = to_str(value, force_string=True)
|
||||||
dbid = dbref(value, reqhash=False)
|
if (value.isdigit() or value.startswith("#")):
|
||||||
if dbid:
|
# we also allow setting using dbrefs, if so we try to load the matching object.
|
||||||
try:
|
# (we assume the object is of the same type as the class holding the field, if
|
||||||
value = cls._default_manager.get(id=dbid)
|
# not a custom handler must be used for that field)
|
||||||
except ObjectDoesNotExist,e:
|
dbid = dbref(value, reqhash=False)
|
||||||
# maybe it is just a name that happens to look like a dbid
|
if dbid:
|
||||||
from src.utils.logger import log_trace
|
model = _GA(cls, "_meta").get_field(fname).model
|
||||||
log_trace()
|
try:
|
||||||
#print "_set wrapper:", fname, value, type(value), cls._get_pk_val(cls._meta)
|
value = model._default_manager.get(id=dbid)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
# maybe it is just a name that happens to look like a dbid
|
||||||
|
pass
|
||||||
_SA(cls, fname, value)
|
_SA(cls, fname, value)
|
||||||
# only use explicit update_fields in save if we actually have a
|
# only use explicit update_fields in save if we actually have a
|
||||||
# primary key assigned already (won't be set when first creating object)
|
# primary key assigned already (won't be set when first creating object)
|
||||||
|
|
@ -128,24 +137,106 @@ class SharedMemoryModelBase(ModelBase):
|
||||||
update_fields = [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
|
update_fields = [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
|
||||||
_GA(cls, "save")(update_fields=update_fields)
|
_GA(cls, "save")(update_fields=update_fields)
|
||||||
|
|
||||||
# create class field wrappers
|
# wrapper factories
|
||||||
fget = lambda cls: _get(cls, fieldname)
|
fget = lambda cls: _get(cls, fieldname)
|
||||||
fset = lambda cls, val: _set(cls, fieldname, val) if editable else _set_nonedit(cls, fieldname, val)
|
if not editable:
|
||||||
|
fset = lambda cls, val: _set_nonedit(cls, fieldname, val)
|
||||||
|
elif foreignkey:
|
||||||
|
fset = lambda cls, val: _set_foreign(cls, fieldname, val)
|
||||||
|
else:
|
||||||
|
fset = lambda cls, val: _set(cls, fieldname, val)
|
||||||
fdel = lambda cls: _del(cls, fieldname) if editable else _del_nonedit(cls,fieldname)
|
fdel = lambda cls: _del(cls, fieldname) if editable else _del_nonedit(cls,fieldname)
|
||||||
type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))
|
# assigning
|
||||||
|
classdict[wrappername] = property(fget, fset, fdel)
|
||||||
|
#type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))
|
||||||
|
|
||||||
# exclude some models that should not auto-create wrapper fields
|
# exclude some models that should not auto-create wrapper fields
|
||||||
if cls.__name__ in ("ServerConfig", "TypeNick"):
|
if cls.__name__ in ("ServerConfig", "TypeNick"):
|
||||||
return
|
return
|
||||||
# dynamically create the wrapper properties for all fields not already handled
|
# dynamically create the wrapper properties for all fields not already handled (manytomanyfields are always handlers)
|
||||||
for field in cls._meta.fields:
|
for fieldname, field in ((fname, field) for fname, field in classdict.items()
|
||||||
fieldname = field.name
|
if fname.startswith("db_") and type(field).__name__ != "ManyToManyField"):
|
||||||
if fieldname.startswith("db_"):
|
foreignkey = type(field).__name__ == "ForeignKey"
|
||||||
wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "")
|
#print fieldname, type(field).__name__, field
|
||||||
if not hasattr(cls, wrappername):
|
wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "", 1)
|
||||||
# makes sure not to overload manually created wrappers on the model
|
if wrappername not in classdict:
|
||||||
#print "wrapping %s -> %s" % (fieldname, wrappername)
|
# makes sure not to overload manually created wrappers on the model
|
||||||
create_wrapper(cls, fieldname, wrappername, editable=field.editable)
|
#print "wrapping %s -> %s" % (fieldname, wrappername)
|
||||||
|
create_wrapper(cls, fieldname, wrappername, editable=field.editable, foreignkey=foreignkey)
|
||||||
|
return super(SharedMemoryModelBase, cls).__new__(cls, classname, bases, classdict, *args, **kwargs)
|
||||||
|
|
||||||
|
#def __init__(cls, *args, **kwargs):
|
||||||
|
# """
|
||||||
|
# Field shortcut creation:
|
||||||
|
# Takes field names db_* and creates property wrappers named without the db_ prefix. So db_key -> key
|
||||||
|
# This wrapper happens on the class level, so there is no overhead when creating objects. If a class
|
||||||
|
# already has a wrapper of the given name, the automatic creation is skipped. Note: Remember to
|
||||||
|
# document this auto-wrapping in the class header, this could seem very much like magic to the user otherwise.
|
||||||
|
# """
|
||||||
|
# super(SharedMemoryModelBase, cls).__init__(*args, **kwargs)
|
||||||
|
# def create_wrapper(cls, fieldname, wrappername, editable=True):
|
||||||
|
# "Helper method to create property wrappers with unique names (must be in separate call)"
|
||||||
|
# def _get(cls, fname):
|
||||||
|
# "Wrapper for getting database field"
|
||||||
|
# value = _GA(cls, fieldname)
|
||||||
|
# if type(value) in (basestring, int, float, bool):
|
||||||
|
# return value
|
||||||
|
# elif hasattr(value, "typeclass"):
|
||||||
|
# return _GA(value, "typeclass")
|
||||||
|
# return value
|
||||||
|
# def _set_nonedit(cls, fname, value):
|
||||||
|
# "Wrapper for blocking editing of field"
|
||||||
|
# raise FieldError("Field %s cannot be edited." % fname)
|
||||||
|
# def _set(cls, fname, value):
|
||||||
|
# "Wrapper for setting database field"
|
||||||
|
# #print "_set:", fname
|
||||||
|
# if hasattr(value, "dbobj"):
|
||||||
|
# value = _GA(value, "dbobj")
|
||||||
|
# elif isinstance(value, basestring) and (value.isdigit() or value.startswith("#")):
|
||||||
|
# # we also allow setting using dbrefs, if so we try to load the matching object.
|
||||||
|
# # (we assume the object is of the same type as the class holding the field, if
|
||||||
|
# # not a custom handler must be used for that field)
|
||||||
|
# dbid = dbref(value, reqhash=False)
|
||||||
|
# if dbid:
|
||||||
|
# try:
|
||||||
|
# value = cls._default_manager.get(id=dbid)
|
||||||
|
# except ObjectDoesNotExist:
|
||||||
|
# # maybe it is just a name that happens to look like a dbid
|
||||||
|
# from src.utils.logger import log_trace
|
||||||
|
# log_trace()
|
||||||
|
# #print "_set wrapper:", fname, value, type(value), cls._get_pk_val(cls._meta)
|
||||||
|
# _SA(cls, fname, value)
|
||||||
|
# # only use explicit update_fields in save if we actually have a
|
||||||
|
# # primary key assigned already (won't be set when first creating object)
|
||||||
|
# update_fields = [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
|
||||||
|
# _GA(cls, "save")(update_fields=update_fields)
|
||||||
|
# def _del_nonedit(cls, fname):
|
||||||
|
# "wrapper for not allowing deletion"
|
||||||
|
# raise FieldError("Field %s cannot be edited." % fname)
|
||||||
|
# def _del(cls, fname):
|
||||||
|
# "Wrapper for clearing database field - sets it to None"
|
||||||
|
# _SA(cls, fname, None)
|
||||||
|
# update_fields = [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
|
||||||
|
# _GA(cls, "save")(update_fields=update_fields)
|
||||||
|
|
||||||
|
# # create class field wrappers
|
||||||
|
# fget = lambda cls: _get(cls, fieldname)
|
||||||
|
# fset = lambda cls, val: _set(cls, fieldname, val) if editable else _set_nonedit(cls, fieldname, val)
|
||||||
|
# fdel = lambda cls: _del(cls, fieldname) if editable else _del_nonedit(cls,fieldname)
|
||||||
|
# type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))
|
||||||
|
|
||||||
|
# # exclude some models that should not auto-create wrapper fields
|
||||||
|
# if cls.__name__ in ("ServerConfig", "TypeNick"):
|
||||||
|
# return
|
||||||
|
# # dynamically create the wrapper properties for all fields not already handled
|
||||||
|
# for field in cls._meta.fields:
|
||||||
|
# fieldname = field.name
|
||||||
|
# if fieldname.startswith("db_"):
|
||||||
|
# wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "")
|
||||||
|
# if not hasattr(cls, wrappername):
|
||||||
|
# # makes sure not to overload manually created wrappers on the model
|
||||||
|
# #print "wrapping %s -> %s" % (fieldname, wrappername)
|
||||||
|
# create_wrapper(cls, fieldname, wrappername, editable=field.editable)
|
||||||
|
|
||||||
class SharedMemoryModel(Model):
|
class SharedMemoryModel(Model):
|
||||||
# CL: setting abstract correctly to allow subclasses to inherit the default
|
# CL: setting abstract correctly to allow subclasses to inherit the default
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,8 @@ class PrettyTable(object):
|
||||||
if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
|
if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
#print "prettytable:", self._rows
|
||||||
|
|
||||||
# Get the rows we need to print, taking into account slicing, sorting, etc.
|
# Get the rows we need to print, taking into account slicing, sorting, etc.
|
||||||
rows = self._get_rows(options)
|
rows = self._get_rows(options)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue