Merged conflicts.
This commit is contained in:
parent
48f1d0b26f
commit
50371b6abd
23 changed files with 39 additions and 39 deletions
|
|
@ -576,7 +576,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
||||||
|
|
||||||
except NoCmdSets:
|
except NoCmdSets:
|
||||||
# Critical error.
|
# Critical error.
|
||||||
logger.log_errmsg("No cmdsets found: %s" % caller)
|
logger.log_err("No cmdsets found: %s" % caller)
|
||||||
error_to.msg(_ERROR_NOCMDSETS, _nomulti=True)
|
error_to.msg(_ERROR_NOCMDSETS, _nomulti=True)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
||||||
# returning an empty error cmdset
|
# returning an empty error cmdset
|
||||||
errstring = errstring.strip()
|
errstring = errstring.strip()
|
||||||
if not no_logging:
|
if not no_logging:
|
||||||
logger.log_errmsg(errstring)
|
logger.log_err(errstring)
|
||||||
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
|
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
|
||||||
emit_to_obj.msg(errstring)
|
emit_to_obj.msg(errstring)
|
||||||
err_cmdset = _ErrorCmdSet()
|
err_cmdset = _ErrorCmdSet()
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ class CmdShutdown(MuxCommand):
|
||||||
announcement = "\nServer is being SHUT DOWN!\n"
|
announcement = "\nServer is being SHUT DOWN!\n"
|
||||||
if self.args:
|
if self.args:
|
||||||
announcement += "%s\n" % self.args
|
announcement += "%s\n" % self.args
|
||||||
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
|
logger.log_info('Server shutdown by %s.' % self.caller.name)
|
||||||
SESSIONS.announce_all(announcement)
|
SESSIONS.announce_all(announcement)
|
||||||
SESSIONS.server.shutdown(mode='shutdown')
|
SESSIONS.server.shutdown(mode='shutdown')
|
||||||
SESSIONS.portal_shutdown()
|
SESSIONS.portal_shutdown()
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,7 @@ def _create_player(session, playername, password, permissions, typeclass=None):
|
||||||
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||||
if not pchannel.connect(new_player):
|
if not pchannel.connect(new_player):
|
||||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||||
logger.log_errmsg(string)
|
logger.log_err(string)
|
||||||
return new_player
|
return new_player
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
|
||||||
pchannel = ChannelDB.objects.get_channel(pchanneldef[0])
|
pchannel = ChannelDB.objects.get_channel(pchanneldef[0])
|
||||||
if not pchannel.connect(new_player):
|
if not pchannel.connect(new_player):
|
||||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||||
logger.log_errmsg(string)
|
logger.log_err(string)
|
||||||
|
|
||||||
if MULTISESSION_MODE < 2:
|
if MULTISESSION_MODE < 2:
|
||||||
# if we only allow one character, create one with the same name as Player
|
# if we only allow one character, create one with the same name as Player
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ class CmdPasswordCreate(Command):
|
||||||
# We are in the middle between logged in and -not, so we have
|
# We are in the middle between logged in and -not, so we have
|
||||||
# to handle tracebacks ourselves at this point. If we don't, we
|
# to handle tracebacks ourselves at this point. If we don't, we
|
||||||
# won't see any errors at all.
|
# won't see any errors at all.
|
||||||
self.caller.msg("An error occurred. Please e-mail an admin if the problem persists."
|
self.caller.msg("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
logger.log_errmsg(traceback.format_exc())
|
logger.log_errmsg(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class HelpEntryManager(models.Manager):
|
||||||
topic.help_category = default_category
|
topic.help_category = default_category
|
||||||
topic.save()
|
topic.save()
|
||||||
string = "Help database moved to category %s" % default_category
|
string = "Help database moved to category %s" % default_category
|
||||||
logger.log_infomsg(string)
|
logger.log_info(string)
|
||||||
|
|
||||||
def search_help(self, ostring, help_category=None):
|
def search_help(self, ostring, help_category=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ def _cache_lockfuncs():
|
||||||
for tup in (tup for tup in inspect.getmembers(mod) if callable(tup[1])):
|
for tup in (tup for tup in inspect.getmembers(mod) if callable(tup[1])):
|
||||||
_LOCKFUNCS[tup[0]] = tup[1]
|
_LOCKFUNCS[tup[0]] = tup[1]
|
||||||
else:
|
else:
|
||||||
logger.log_errmsg("Couldn't load %s from PERMISSION_FUNC_MODULES." % modulepath)
|
logger.log_err("Couldn't load %s from PERMISSION_FUNC_MODULES." % modulepath)
|
||||||
|
|
||||||
#
|
#
|
||||||
# pre-compiled regular expressions
|
# pre-compiled regular expressions
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ class ObjectDBManager(TypedObjectManager):
|
||||||
return []
|
return []
|
||||||
except ValueError:
|
except ValueError:
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
logger.log_errmsg("The property '%s' does not support search criteria of the type %s." % (property_name, type(property_value)))
|
logger.log_err("The property '%s' does not support search criteria of the type %s." % (property_name, type(property_value)))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@returns_typeclass_list
|
@returns_typeclass_list
|
||||||
|
|
|
||||||
|
|
@ -269,8 +269,8 @@ class ObjectDB(TypedObject):
|
||||||
raise #RuntimeError(errmsg)
|
raise #RuntimeError(errmsg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
|
errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
|
||||||
logger.log_errmsg(errmsg)
|
logger.log_trace(errmsg)
|
||||||
raise #Exception(errmsg)
|
raise
|
||||||
|
|
||||||
def __location_del(self):
|
def __location_del(self):
|
||||||
"Cleanly delete the location reference"
|
"Cleanly delete the location reference"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from evennia.scripts.scripthandler import ScriptHandler
|
||||||
from evennia.commands import cmdset, command
|
from evennia.commands import cmdset, command
|
||||||
from evennia.commands.cmdsethandler import CmdSetHandler
|
from evennia.commands.cmdsethandler import CmdSetHandler
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
from evennia.utils.logger import log_trace, log_errmsg
|
from evennia.utils import logger
|
||||||
from evennia.utils.utils import (variable_from_module, lazy_property,
|
from evennia.utils.utils import (variable_from_module, lazy_property,
|
||||||
make_iter, to_str, to_unicode)
|
make_iter, to_str, to_unicode)
|
||||||
|
|
||||||
|
|
@ -432,13 +432,13 @@ class DefaultObject(ObjectDB):
|
||||||
try:
|
try:
|
||||||
from_obj.at_msg_send(text=text, to_obj=self, **kwargs)
|
from_obj.at_msg_send(text=text, to_obj=self, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
log_trace()
|
logger.log_trace()
|
||||||
try:
|
try:
|
||||||
if not self.at_msg_receive(text=text, **kwargs):
|
if not self.at_msg_receive(text=text, **kwargs):
|
||||||
# if at_msg_receive returns false, we abort message to this object
|
# if at_msg_receive returns false, we abort message to this object
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
log_trace()
|
logger.log_trace()
|
||||||
|
|
||||||
# session relay
|
# session relay
|
||||||
kwargs['_nomulti'] = kwargs.get('_nomulti', True)
|
kwargs['_nomulti'] = kwargs.get('_nomulti', True)
|
||||||
|
|
@ -522,7 +522,7 @@ class DefaultObject(ObjectDB):
|
||||||
"Simple log helper method"
|
"Simple log helper method"
|
||||||
trc = traceback.format_exc()
|
trc = traceback.format_exc()
|
||||||
errstring = "%s%s" % (trc, string)
|
errstring = "%s%s" % (trc, string)
|
||||||
log_trace()
|
logger.log_trace()
|
||||||
self.msg(errstring)
|
self.msg(errstring)
|
||||||
|
|
||||||
errtxt = _("Couldn't perform move ('%s'). Contact an admin.")
|
errtxt = _("Couldn't perform move ('%s'). Contact an admin.")
|
||||||
|
|
@ -585,7 +585,7 @@ class DefaultObject(ObjectDB):
|
||||||
self.location = destination
|
self.location = destination
|
||||||
except Exception:
|
except Exception:
|
||||||
emit_to_obj.msg(errtxt % "location change")
|
emit_to_obj.msg(errtxt % "location change")
|
||||||
log_trace()
|
logger.log_trace()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
|
|
@ -642,7 +642,7 @@ class DefaultObject(ObjectDB):
|
||||||
default_home = None
|
default_home = None
|
||||||
except Exception:
|
except Exception:
|
||||||
string = _("Could not find default home '(#%d)'.")
|
string = _("Could not find default home '(#%d)'.")
|
||||||
log_errmsg(string % default_home_id)
|
logger.log_err(string % default_home_id)
|
||||||
default_home = None
|
default_home = None
|
||||||
|
|
||||||
for obj in self.contents:
|
for obj in self.contents:
|
||||||
|
|
@ -658,7 +658,7 @@ class DefaultObject(ObjectDB):
|
||||||
string += "now has a null location."
|
string += "now has a null location."
|
||||||
obj.location = None
|
obj.location = None
|
||||||
obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
|
obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
|
||||||
log_errmsg(string % (obj.name, obj.dbid))
|
logger.log_err(string % (obj.name, obj.dbid))
|
||||||
return
|
return
|
||||||
|
|
||||||
if obj.has_player:
|
if obj.has_player:
|
||||||
|
|
|
||||||
|
|
@ -673,7 +673,7 @@ class DefaultPlayer(PlayerDB):
|
||||||
if _CONNECT_CHANNEL:
|
if _CONNECT_CHANNEL:
|
||||||
_CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message))
|
_CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message))
|
||||||
else:
|
else:
|
||||||
logger.log_infomsg("[%s]: %s" % (now, message))
|
logger.log_info("[%s]: %s" % (now, message))
|
||||||
|
|
||||||
def at_post_login(self, sessid=None):
|
def at_post_login(self, sessid=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class ScriptHandler(object):
|
||||||
script = create.create_script(scriptclass, key=key, obj=self.obj,
|
script = create.create_script(scriptclass, key=key, obj=self.obj,
|
||||||
autostart=autostart)
|
autostart=autostart)
|
||||||
if not script:
|
if not script:
|
||||||
logger.log_errmsg("Script %s could not be created and/or started." % scriptclass)
|
logger.log_err("Script %s could not be created and/or started." % scriptclass)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ class DefaultScript(ScriptBase):
|
||||||
self.db_obj.msg(estring)
|
self.db_obj.msg(estring)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.log_errmsg(estring)
|
logger.log_err(estring)
|
||||||
|
|
||||||
def _step_callback(self):
|
def _step_callback(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class ServerConfig(WeakSharedMemoryModel):
|
||||||
"Setter. Allows for self.value = value"
|
"Setter. Allows for self.value = value"
|
||||||
if utils.has_parent('django.db.models.base.Model', value):
|
if utils.has_parent('django.db.models.base.Model', value):
|
||||||
# we have to protect against storing db objects.
|
# we have to protect against storing db objects.
|
||||||
logger.log_errmsg("ServerConfig cannot store db objects! (%s)" % value)
|
logger.log_err("ServerConfig cannot store db objects! (%s)" % value)
|
||||||
return
|
return
|
||||||
self.db_value = pickle.dumps(value)
|
self.db_value = pickle.dumps(value)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
|
||||||
# Only support Plain text passwords.
|
# Only support Plain text passwords.
|
||||||
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
|
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
|
||||||
|
|
||||||
logger.log_infomsg("IMC2: AUTH< %s" % line)
|
logger.log_info("IMC2: AUTH< %s" % line)
|
||||||
|
|
||||||
line_split = line.split(' ')
|
line_split = line.split(' ')
|
||||||
pw_present = line_split[0] == 'PW'
|
pw_present = line_split[0] == 'PW'
|
||||||
|
|
@ -236,21 +236,21 @@ class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
|
||||||
|
|
||||||
if "reject" in line_split:
|
if "reject" in line_split:
|
||||||
auth_message = _("IMC2 server rejected connection.")
|
auth_message = _("IMC2 server rejected connection.")
|
||||||
logger.log_infomsg(auth_message)
|
logger.log_info(auth_message)
|
||||||
return
|
return
|
||||||
|
|
||||||
if pw_present:
|
if pw_present:
|
||||||
self.server_name = line_split[1]
|
self.server_name = line_split[1]
|
||||||
self.network_name = line_split[4]
|
self.network_name = line_split[4]
|
||||||
elif autosetup_present:
|
elif autosetup_present:
|
||||||
logger.log_infomsg(_("IMC2: Autosetup response found."))
|
logger.log_info(_("IMC2: Autosetup response found."))
|
||||||
self.server_name = line_split[1]
|
self.server_name = line_split[1]
|
||||||
self.network_name = line_split[3]
|
self.network_name = line_split[3]
|
||||||
self.is_authenticated = True
|
self.is_authenticated = True
|
||||||
self.sequence = int(time())
|
self.sequence = int(time())
|
||||||
|
|
||||||
# Log to stdout and notify over MUDInfo.
|
# Log to stdout and notify over MUDInfo.
|
||||||
logger.log_infomsg('IMC2: Authenticated to %s' % self.factory.network)
|
logger.log_info('IMC2: Authenticated to %s' % self.factory.network)
|
||||||
|
|
||||||
# Ask to see what other MUDs are connected.
|
# Ask to see what other MUDs are connected.
|
||||||
self._send_packet(pck.IMC2PacketKeepAliveRequest())
|
self._send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||||
|
|
@ -274,7 +274,7 @@ class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
|
||||||
self.uid = int(self.factory.uid)
|
self.uid = int(self.factory.uid)
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
self.factory.sessionhandler.connect(self)
|
self.factory.sessionhandler.connect(self)
|
||||||
logger.log_infomsg("IMC2 bot connected to %s." % self.network)
|
logger.log_info("IMC2 bot connected to %s." % self.network)
|
||||||
# Send authentication packet. The reply will be caught by lineReceived
|
# Send authentication packet. The reply will be caught by lineReceived
|
||||||
self._send_packet(pck.IMC2PacketAuthPlaintext())
|
self._send_packet(pck.IMC2PacketAuthPlaintext())
|
||||||
|
|
||||||
|
|
@ -461,7 +461,7 @@ class IMC2BotFactory(protocol.ReconnectingClientFactory):
|
||||||
def start(self):
|
def start(self):
|
||||||
"Connect session to sessionhandler"
|
"Connect session to sessionhandler"
|
||||||
def errback(fail):
|
def errback(fail):
|
||||||
logger.log_errmsg(fail.value)
|
logger.log_err(fail.value)
|
||||||
|
|
||||||
if self.port:
|
if self.port:
|
||||||
service = internet.TCPClient(self.network, int(self.port), self)
|
service = internet.TCPClient(self.network, int(self.port), self)
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,8 @@ class IRCBot(irc.IRCClient, Session):
|
||||||
self.uid = int(self.factory.uid)
|
self.uid = int(self.factory.uid)
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
self.factory.sessionhandler.connect(self)
|
self.factory.sessionhandler.connect(self)
|
||||||
logger.log_infomsg("IRC bot '%s' connected to %s at %s:%s." % (self.nickname, self.channel,
|
logger.log_info("IRC bot '%s' connected to %s at %s:%s." % (self.nickname, self.channel,
|
||||||
self.network, self.port))
|
self.network, self.port))
|
||||||
|
|
||||||
def disconnect(self, reason=None):
|
def disconnect(self, reason=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -276,7 +276,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
connector (Connector): Represents the connection.
|
connector (Connector): Represents the connection.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.log_infomsg("(re)connecting to %s" % self.channel)
|
logger.log_info("(re)connecting to %s" % self.channel)
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
def clientConnectionFailed(self, connector, reason):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ class RSSReader(Session):
|
||||||
|
|
||||||
def _errback(self, fail):
|
def _errback(self, fail):
|
||||||
"Report error"
|
"Report error"
|
||||||
logger.log_errmsg("RSS feed error: %s" % fail.value)
|
logger.log_err("RSS feed error: %s" % fail.value)
|
||||||
|
|
||||||
def update(self, init=False):
|
def update(self, init=False):
|
||||||
"""
|
"""
|
||||||
|
|
@ -139,7 +139,7 @@ class RSSBotFactory(object):
|
||||||
Called by portalsessionhandler. Starts te bot.
|
Called by portalsessionhandler. Starts te bot.
|
||||||
"""
|
"""
|
||||||
def errback(fail):
|
def errback(fail):
|
||||||
logger.log_errmsg(fail.value)
|
logger.log_err(fail.value)
|
||||||
|
|
||||||
# set up session and connect it to sessionhandler
|
# set up session and connect it to sessionhandler
|
||||||
self.bot.init_session("rssbot", self.url, self.sessionhandler)
|
self.bot.init_session("rssbot", self.url, self.sessionhandler)
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ class ServerSession(Session):
|
||||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.log_infomsg(message)
|
logger.log_info(message)
|
||||||
|
|
||||||
def get_client_size(self):
|
def get_client_size(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ def create_help_entry(key, entrytext, category="General", locks=None):
|
||||||
return new_help
|
return new_help
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
string = "Could not add help entry: key '%s' already exists." % key
|
string = "Could not add help entry: key '%s' already exists." % key
|
||||||
logger.log_errmsg(string)
|
logger.log_err(string)
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ class _SaverMutable(object):
|
||||||
elif self._db_obj:
|
elif self._db_obj:
|
||||||
self._db_obj.value = self
|
self._db_obj.value = self
|
||||||
else:
|
else:
|
||||||
logger.log_errmsg("_SaverMutable %s has no root Attribute to save to." % self)
|
logger.log_err("_SaverMutable %s has no root Attribute to save to." % self)
|
||||||
|
|
||||||
def _convert_mutables(self, data):
|
def _convert_mutables(self, data):
|
||||||
"converts mutables to Saver* variants and assigns .parent property"
|
"converts mutables to Saver* variants and assigns .parent property"
|
||||||
|
|
|
||||||
|
|
@ -524,8 +524,8 @@ def conditional_flush(max_rmem, force=False):
|
||||||
|
|
||||||
if ((now - LAST_FLUSH) < AUTO_FLUSH_MIN_INTERVAL) and not force:
|
if ((now - LAST_FLUSH) < AUTO_FLUSH_MIN_INTERVAL) and not force:
|
||||||
# too soon after last flush.
|
# too soon after last flush.
|
||||||
logger.log_warnmsg("Warning: Idmapper flush called more than "\
|
logger.log_warn("Warning: Idmapper flush called more than "\
|
||||||
"once in %s min interval. Check memory usage." % (AUTO_FLUSH_MIN_INTERVAL/60.0))
|
"once in %s min interval. Check memory usage." % (AUTO_FLUSH_MIN_INTERVAL/60.0))
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
|
|
|
||||||
|
|
@ -1176,7 +1176,7 @@ def init_new_player(player):
|
||||||
Deprecated.
|
Deprecated.
|
||||||
"""
|
"""
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
logger.log_depmsg("evennia.utils.utils.init_new_player is DEPRECATED and should not be used.")
|
logger.log_dep("evennia.utils.utils.init_new_player is DEPRECATED and should not be used.")
|
||||||
|
|
||||||
|
|
||||||
def string_similarity(string1, string2):
|
def string_similarity(string1, string2):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue