Resolve merge conflicts

This commit is contained in:
Griatch 2020-05-16 15:38:09 +02:00
commit 4bfaa154d9
31 changed files with 1228 additions and 595 deletions

12
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: griatch
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://www.paypal.me/GriatchEvennia

View file

@ -13,9 +13,10 @@
- Added `content_types` indexing to DefaultObject's ContentsHandler. (volund) - Added `content_types` indexing to DefaultObject's ContentsHandler. (volund)
- Made most of the networking classes such as Protocols and the SessionHandlers - Made most of the networking classes such as Protocols and the SessionHandlers
replaceable via `settings.py` for modding enthusiasts. (volund) replaceable via `settings.py` for modding enthusiasts. (volund)
- The `initial_setup.py` file can now be substituted in `settings.py` to customize
initial game database state. (volund)
- Added new Traits contrib, converted and expanded from Ainneve project. - Added new Traits contrib, converted and expanded from Ainneve project.
### Already in master ### Already in master
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `py` command now reroutes stdout to output results in-game client. `py` - `py` command now reroutes stdout to output results in-game client. `py`

View file

@ -32,7 +32,7 @@ from evennia.server.signals import (
SIGNAL_OBJECT_POST_PUPPET, SIGNAL_OBJECT_POST_PUPPET,
SIGNAL_OBJECT_POST_UNPUPPET, SIGNAL_OBJECT_POST_UNPUPPET,
) )
from evennia.typeclasses.attributes import NickHandler from evennia.typeclasses.attributes import NickHandler, ModelAttributeBackend
from evennia.scripts.scripthandler import ScriptHandler from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.utils.optionhandler import OptionHandler from evennia.utils.optionhandler import OptionHandler
@ -199,7 +199,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
@lazy_property @lazy_property
def nicks(self): def nicks(self):
return NickHandler(self) return NickHandler(self, ModelAttributeBackend)
@lazy_property @lazy_property
def sessions(self): def sessions(self):
@ -275,11 +275,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
raise RuntimeError("Session not found") raise RuntimeError("Session not found")
if self.get_puppet(session) == obj: if self.get_puppet(session) == obj:
# already puppeting this object # already puppeting this object
self.msg("You are already puppeting this object.") self.msg(_("You are already puppeting this object."))
return return
if not obj.access(self, "puppet"): if not obj.access(self, "puppet"):
# no access # no access
self.msg(f"You don't have permission to puppet '{obj.key}'.") self.msg(_("You don't have permission to puppet '{key}'.").format(key=obj.key))
return return
if obj.account: if obj.account:
# object already puppeted # object already puppeted
@ -295,12 +295,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
else: else:
txt1 = f"Taking over |c{obj.name}|n from another of your sessions." txt1 = f"Taking over |c{obj.name}|n from another of your sessions."
txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n" txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
self.msg(txt1, session=session) self.msg(_(txt1), session=session)
self.msg(txt2, session=obj.sessions.all()) self.msg(_(txt2), session=obj.sessions.all())
self.unpuppet_object(obj.sessions.get()) self.unpuppet_object(obj.sessions.get())
elif obj.account.is_connected: elif obj.account.is_connected:
# controlled by another account # controlled by another account
self.msg(f"|c{obj.key}|R is already puppeted by another Account.") self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key))
return return
# do the puppeting # do the puppeting
@ -496,7 +496,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# See if authentication is currently being throttled # See if authentication is currently being throttled
if ip and LOGIN_THROTTLE.check(ip): if ip and LOGIN_THROTTLE.check(ip):
errors.append("Too many login failures; please try again in a few minutes.") errors.append(_("Too many login failures; please try again in a few minutes."))
# With throttle active, do not log continued hits-- it is a # With throttle active, do not log continued hits-- it is a
# waste of storage and can be abused to make your logs harder to # waste of storage and can be abused to make your logs harder to
@ -508,9 +508,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if banned: if banned:
# this is a banned IP or name! # this is a banned IP or name!
errors.append( errors.append(
_(
"|rYou have been banned and cannot continue from here." "|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x" "\nIf you feel this ban is in error, please email an admin.|x"
) )
)
logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).") logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.") LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.")
return None, errors return None, errors
@ -519,7 +521,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
account = authenticate(username=username, password=password) account = authenticate(username=username, password=password)
if not account: if not account:
# User-facing message # User-facing message
errors.append("Username and/or password is incorrect.") errors.append(_("Username and/or password is incorrect."))
# Log auth failures while throttle is inactive # Log auth failures while throttle is inactive
logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).") logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")
@ -688,7 +690,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
ip = kwargs.get("ip", "") ip = kwargs.get("ip", "")
if ip and CREATION_THROTTLE.check(ip): if ip and CREATION_THROTTLE.check(ip):
errors.append( errors.append(
"You are creating too many accounts. Please log into an existing account." _("You are creating too many accounts. Please log into an existing account.")
) )
return None, errors return None, errors
@ -716,7 +718,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
banned = cls.is_banned(username=username, ip=ip) banned = cls.is_banned(username=username, ip=ip)
if banned: if banned:
# this is a banned IP or name! # this is a banned IP or name!
string = ( string = _(
"|rYou have been banned and cannot continue from here." "|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x" "\nIf you feel this ban is in error, please email an admin.|x"
) )
@ -733,8 +735,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
except Exception as e: except Exception as e:
errors.append( errors.append(
_(
"There was an error creating the Account. If this problem persists, contact an admin." "There was an error creating the Account. If this problem persists, contact an admin."
) )
)
logger.log_trace() logger.log_trace()
return None, errors return None, errors
@ -785,7 +789,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# 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, # to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all. # we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.") errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
logger.log_trace() logger.log_trace()
# Update the throttle to indicate a new account was created from this IP # Update the throttle to indicate a new account was created from this IP
@ -1253,21 +1257,21 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if session: if session:
session.msg(logged_in={}) session.msg(logged_in={})
self._send_to_connect_channel(f"|G{self.key} connected|n") self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
if _MULTISESSION_MODE == 0: if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We # in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any # try to auto-connect to our last conneted object, if any
try: try:
self.puppet_object(session, self.db._last_puppet) self.puppet_object(session, self.db._last_puppet)
except RuntimeError: except RuntimeError:
self.msg("The Character does not exist.") self.msg(_("The Character does not exist."))
return return
elif _MULTISESSION_MODE == 1: elif _MULTISESSION_MODE == 1:
# in this mode all sessions connect to the same puppet. # in this mode all sessions connect to the same puppet.
try: try:
self.puppet_object(session, self.db._last_puppet) self.puppet_object(session, self.db._last_puppet)
except RuntimeError: except RuntimeError:
self.msg("The Character does not exist.") self.msg(_("The Character does not exist."))
return return
elif _MULTISESSION_MODE in (2, 3): elif _MULTISESSION_MODE in (2, 3):
# In this mode we by default end up at a character selection # In this mode we by default end up at a character selection
@ -1305,7 +1309,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
""" """
reason = f" ({reason if reason else ''})" reason = f" ({reason if reason else ''})"
self._send_to_connect_channel(f"|R{self.key} disconnected{reason}|n") self._send_to_connect_channel(
_("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason)
)
def at_post_disconnect(self, **kwargs): def at_post_disconnect(self, **kwargs):
""" """
@ -1411,7 +1417,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if hasattr(target, "return_appearance"): if hasattr(target, "return_appearance"):
return target.return_appearance(self) return target.return_appearance(self)
else: else:
return "{} has no in-game appearance.".format(target) return _("{target} has no in-game appearance.").format(target=target)
else: else:
# list of targets - make list to disconnect from db # list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else [] characters = list(tar for tar in target if tar) if target else []
@ -1454,8 +1460,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if is_su or len(characters) < charmax: if is_su or len(characters) < charmax:
if not characters: if not characters:
result.append( result.append(
_(
"\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one." "\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one."
) )
)
else: else:
result.append("\n |w@charcreate <name> [=description]|n - create new character") result.append("\n |w@charcreate <name> [=description]|n - create new character")
result.append( result.append(
@ -1534,7 +1542,7 @@ class DefaultGuest(DefaultAccount):
# check if guests are enabled. # check if guests are enabled.
if not settings.GUEST_ENABLED: if not settings.GUEST_ENABLED:
errors.append("Guest accounts are not enabled on this server.") errors.append(_("Guest accounts are not enabled on this server."))
return None, errors return None, errors
try: try:
@ -1544,7 +1552,7 @@ class DefaultGuest(DefaultAccount):
username = name username = name
break break
if not username: if not username:
errors.append("All guest accounts are in use. Please try again later.") errors.append(_("All guest accounts are in use. Please try again later."))
if ip: if ip:
LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.") LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.")
return None, errors return None, errors
@ -1572,7 +1580,7 @@ class DefaultGuest(DefaultAccount):
# 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, # to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all. # we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.") errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
logger.log_trace() logger.log_trace()
return None, errors return None, errors
@ -1589,7 +1597,7 @@ class DefaultGuest(DefaultAccount):
overriding the call (unused by default). overriding the call (unused by default).
""" """
self._send_to_connect_channel(f"|G{self.key} connected|n") self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
self.puppet_object(session, self.db._last_puppet) self.puppet_object(session, self.db._last_puppet)
def at_server_shutdown(self): def at_server_shutdown(self):

View file

@ -10,6 +10,7 @@ from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
from evennia.utils import search from evennia.utils import search
from evennia.utils import utils from evennia.utils import utils
from django.utils.translation import gettext as _
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT _IDLE_TIMEOUT = settings.IDLE_TIMEOUT
@ -328,7 +329,9 @@ class IRCBot(Bot):
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
for obj in self._nicklist_callers: for obj in self._nicklist_callers:
obj.msg(f"Nicks at {chstr}:\n {nicklist}") obj.msg(
_("Nicks at {chstr}:\n {nicklist}").format(chstr=chstr, nicklist=nicklist)
)
self._nicklist_callers = [] self._nicklist_callers = []
return return
@ -337,7 +340,11 @@ class IRCBot(Bot):
if hasattr(self, "_ping_callers") and self._ping_callers: if hasattr(self, "_ping_callers") and self._ping_callers:
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})" chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
for obj in self._ping_callers: for obj in self._ping_callers:
obj.msg(f"IRC ping return from {chstr} took {kwargs['timing']}s.") obj.msg(
_("IRC ping return from {chstr} took {time}s.").format(
chstr=chstr, time=kwargs["timing"]
)
)
self._ping_callers = [] self._ping_callers = []
return return

View file

@ -743,7 +743,9 @@ def cmdhandler(
sysarg = raw_string sysarg = raw_string
else: else:
# fallback to default error text # fallback to default error text
sysarg = _("Command '%s' is not available.") % raw_string sysarg = _("Command '{command}' is not available.").format(
command=raw_string
)
suggestions = string_suggestions( suggestions = string_suggestions(
raw_string, raw_string,
cmdset.get_all_cmd_keys_and_aliases(caller), cmdset.get_all_cmd_keys_and_aliases(caller),
@ -751,8 +753,8 @@ def cmdhandler(
maxnum=3, maxnum=3,
) )
if suggestions: if suggestions:
sysarg += _(" Maybe you meant %s?") % utils.list_to_string( sysarg += _(" Maybe you meant {command}?").format(
suggestions, _("or"), addquote=True command=utils.list_to_string(suggestions, _("or"), addquote=True)
) )
else: else:
sysarg += _(' Type "help" for help.') sysarg += _(' Type "help" for help.')

View file

@ -184,7 +184,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
raise exc.with_traceback(tb) raise exc.with_traceback(tb)
else: else:
# try next suggested path # try next suggested path
errstring += _("\n(Unsuccessfully tried '%s')." % python_path) errstring += _("\n(Unsuccessfully tried '{path}').").format(
path=python_path
)
continue continue
try: try:
cmdsetclass = getattr(module, classname) cmdsetclass = getattr(module, classname)
@ -194,7 +196,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
dum, dum, tb = sys.exc_info() dum, dum, tb = sys.exc_info()
raise exc.with_traceback(tb) raise exc.with_traceback(tb)
else: else:
errstring += _("\n(Unsuccessfully tried '%s')." % python_path) errstring += _("\n(Unsuccessfully tried '{path}').").format(
path=python_path
)
continue continue
_CACHED_CMDSETS[python_path] = cmdsetclass _CACHED_CMDSETS[python_path] = cmdsetclass

View file

@ -119,17 +119,17 @@ class ChannelCommand(command.Command):
caller = caller if not hasattr(caller, "account") else caller.account caller = caller if not hasattr(caller, "account") else caller.account
unmuted = channel.unmute(caller) unmuted = channel.unmute(caller)
if unmuted: if unmuted:
self.msg("You start listening to %s." % channel) self.msg(_("You start listening to %s.") % channel)
return return
self.msg("You were already listening to %s." % channel) self.msg(_("You were already listening to %s.") % channel)
return return
if msg == "off": if msg == "off":
caller = caller if not hasattr(caller, "account") else caller.account caller = caller if not hasattr(caller, "account") else caller.account
muted = channel.mute(caller) muted = channel.mute(caller)
if muted: if muted:
self.msg("You stop listening to %s." % channel) self.msg(_("You stop listening to %s.") % channel)
return return
self.msg("You were already not listening to %s." % channel) self.msg(_("You were already not listening to %s.") % channel)
return return
if self.history_start is not None: if self.history_start is not None:
# Try to view history # Try to view history
@ -144,7 +144,7 @@ class ChannelCommand(command.Command):
else: else:
caller = caller if not hasattr(caller, "account") else caller.account caller = caller if not hasattr(caller, "account") else caller.account
if caller in channel.mutelist: if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel) self.msg(_("You currently have %s muted.") % channel)
return return
channel.msg(msg, senders=self.caller, online=True) channel.msg(msg, senders=self.caller, online=True)

View file

@ -289,7 +289,7 @@ class CmdCreatePuzzleRecipe(MuxCommand):
proto_parts = [proto_def(obj) for obj in parts] proto_parts = [proto_def(obj) for obj in parts]
proto_results = [proto_def(obj) for obj in results] proto_results = [proto_def(obj) for obj in results]
puzzle = create_script(PuzzleRecipe, key=puzzle_name) puzzle = create_script(PuzzleRecipe, key=puzzle_name, persistent=True)
puzzle.save_recipe(puzzle_name, proto_parts, proto_results) puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:]) puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])
@ -488,7 +488,7 @@ class CmdArmPuzzle(MuxCommand):
Notes: Notes:
Create puzzles with `@puzzle`; get list of Create puzzles with `@puzzle`; get list of
defined puzzles using `@lspuzlerecipies`. defined puzzles using `@lspuzzlerecipes`.
""" """

View file

@ -131,7 +131,9 @@ class HelpEntryManager(TypedObjectManager):
for topic in topics: for topic in topics:
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 {default_category}").format(
default_category=default_category
)
logger.log_info(string) logger.log_info(string)
def search_help(self, ostring, help_category=None): def search_help(self, ostring, help_category=None):

Binary file not shown.

View file

@ -0,0 +1,299 @@
msgid ""
msgstr ""
"Project-Id-Version: Evennia Russian Translation v0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-02-20 12:13+0000\n"
"PO-Revision-Date: 2020-04-19 18:32+0000\n"
"Last-Translator: 3eluk\n"
"Language-Team: Russian (Russia)\n"
"Language: ru-RU\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && "
"n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Loco-Source-Locale: ru_RU\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Parser: loco_parse_po"
#: accounts/accounts.py:440
msgid "Account being deleted."
msgstr "Аккаунт удаляется."
#: commands/cmdhandler.py:681
msgid "There were multiple matches."
msgstr "Здесь было несколько совпадений."
#: commands/cmdhandler.py:704
#, python-format
msgid "Command '%s' is not available."
msgstr "Команда '%s' недоступна."
#: commands/cmdhandler.py:709
#, python-format
msgid " Maybe you meant %s?"
msgstr "Возможно, вы имели ввиду %s?"
#: commands/cmdhandler.py:709
msgid "or"
msgstr "или"
#: commands/cmdhandler.py:711
msgid " Type \"help\" for help."
msgstr " Введи \"справка\" для получения помощи."
#: commands/cmdsethandler.py:89
msgid ""
"{traceback}\n"
"Error loading cmdset '{path}'\n"
"(Traceback was logged {timestamp})"
msgstr ""
#: commands/cmdsethandler.py:94
msgid ""
"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n"
"(Traceback was logged {timestamp})"
msgstr ""
#: commands/cmdsethandler.py:98
msgid ""
"{traceback}\n"
"SyntaxError encountered when loading cmdset '{path}'.\n"
"(Traceback was logged {timestamp})"
msgstr ""
#: commands/cmdsethandler.py:103
msgid ""
"{traceback}\n"
"Compile/Run error when loading cmdset '{path}'.\",\n"
"(Traceback was logged {timestamp})"
msgstr ""
#: commands/cmdsethandler.py:108
msgid ""
"\n"
"Error encountered for cmdset at path '{path}'.\n"
"Replacing with fallback '{fallback_path}'.\n"
msgstr ""
#: commands/cmdsethandler.py:114
msgid "Fallback path '{fallback_path}' failed to generate a cmdset."
msgstr ""
#: commands/cmdsethandler.py:182 commands/cmdsethandler.py:192
#, python-format
msgid ""
"\n"
"(Unsuccessfully tried '%s')."
msgstr ""
"\n"
"(Безуспешно пробую '%s')."
#: commands/cmdsethandler.py:311
msgid "custom {mergetype} on cmdset '{cmdset}'"
msgstr ""
#: commands/cmdsethandler.py:314
msgid " <Merged {mergelist} {mergetype}, prio {prio}>: {current}"
msgstr ""
#: commands/cmdsethandler.py:322
msgid ""
" <{key} ({mergetype}, prio {prio}, {permstring})>:\n"
" {keylist}"
msgstr ""
#: commands/cmdsethandler.py:426
msgid "Only CmdSets can be added to the cmdsethandler!"
msgstr ""
#: comms/channelhandler.py:100
msgid "Say what?"
msgstr "Сказать что?"
#: comms/channelhandler.py:105
#, python-format
msgid "Channel '%s' not found."
msgstr "Канал '%s' не обнаружен."
#: comms/channelhandler.py:108
#, python-format
msgid "You are not connected to channel '%s'."
msgstr "Ты не соединён с каналом '%s'."
#: comms/channelhandler.py:112
#, python-format
msgid "You are not permitted to send to channel '%s'."
msgstr "У тебя нет разрешения слать в канал '%s'."
#: comms/channelhandler.py:155
msgid " (channel)"
msgstr " (канал)"
#: locks/lockhandler.py:236
#, python-format
msgid "Lock: lock-function '%s' is not available."
msgstr ""
#: locks/lockhandler.py:249
#, python-format
msgid "Lock: definition '%s' has syntax errors."
msgstr ""
#: locks/lockhandler.py:253
#, python-format
msgid ""
"LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)"
"s' to '%(goal)s' "
msgstr ""
#: locks/lockhandler.py:320
msgid "Lock: '{lockdef}' contains no colon (:)."
msgstr ""
#: locks/lockhandler.py:328
msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)."
msgstr ""
#: locks/lockhandler.py:336
msgid "Lock: '{lockdef}' has mismatched parentheses."
msgstr ""
#: locks/lockhandler.py:343
msgid "Lock: '{lockdef}' has no valid lock functions."
msgstr ""
#: objects/objects.py:732
#, python-format
msgid "Couldn't perform move ('%s'). Contact an admin."
msgstr "Не удалось выполнить действие ('%s'). Свяжитесь с администратором."
#: objects/objects.py:742
msgid "The destination doesn't exist."
msgstr "Такой точки назначения нету."
#: objects/objects.py:833
#, python-format
msgid "Could not find default home '(#%d)'."
msgstr "Не обнаружен дом по умолчанию '(#%d)'."
#: objects/objects.py:849
msgid "Something went wrong! You are dumped into nowhere. Contact an admin."
msgstr ""
"Что-то пошло не так! Тебя выбрасывает в пустоту. Свяжитесь с администратором."
#: objects/objects.py:915
#, python-format
msgid "Your character %s has been destroyed."
msgstr "Ваш персонаж %s был уничтожен."
#: scripts/scripthandler.py:53
#, python-format
msgid ""
"\n"
" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s"
msgstr ""
#: scripts/scripts.py:205
#, python-format
msgid ""
"Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'."
msgstr ""
#: server/initial_setup.py:28
msgid ""
"\n"
"Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if "
"you need\n"
"help, want to contribute, report issues or just join the community.\n"
"As Account #1 you can create a demo/tutorial area with |w@batchcommand "
"tutorial_world.build|n.\n"
" "
msgstr ""
"\n"
"Добро пожаловать в твою новую игру, основанную на |wEvennia|n! Посети http:"
"//www.evennia.com\n"
"если тебе нужна помощь, хочешь помочь, сообщить об ошибках, lили просто "
"присоединиться к сообществу.\n"
"Как Аккаунт №1, ты можешь создать зону для демонстрации/обучения командой "
"|w@batchcommand tutorial_world.build|n.\n"
" "
#: server/initial_setup.py:92
msgid "This is User #1."
msgstr "Это Пользователь №1."
#: server/initial_setup.py:105
msgid "Limbo"
msgstr "Лимб"
#: server/server.py:139
msgid "idle timeout exceeded"
msgstr "время бездействия превышено"
#: server/sessionhandler.py:386
msgid " ... Server restarted."
msgstr " ... Сервер перезапущен."
#: server/sessionhandler.py:606
msgid "Logged in from elsewhere. Disconnecting."
msgstr "Выполнено соединение в другом месте. Отключение."
#: server/sessionhandler.py:634
msgid "Idle timeout exceeded, disconnecting."
msgstr "Время бездействия превышено, отключение."
#: server/validators.py:50
#, python-format
msgid ""
"%s From a terminal client, you can also use a phrase of multiple words if "
"you enclose the password in double quotes."
msgstr ""
"%s Если вы используете терминал, вы можете использовать фразу из нескольких "
"слов если возьмёте пароль в двойные скобки."
#: utils/evmenu.py:192
msgid ""
"Menu node '{nodename}' is either not implemented or caused an error. Make "
"another choice."
msgstr ""
#: utils/evmenu.py:194
msgid "Error in menu node '{nodename}'."
msgstr ""
#: utils/evmenu.py:195
msgid "No description."
msgstr "Нет описания."
#: utils/evmenu.py:196
msgid "Commands: <menu option>, help, quit"
msgstr "Команды: <menu option>, справка, выход"
#: utils/evmenu.py:197
msgid "Commands: <menu option>, help"
msgstr "Команды: <menu option>, справка"
#: utils/evmenu.py:198
msgid "Commands: help, quit"
msgstr ""
#: utils/evmenu.py:199
msgid "Commands: help"
msgstr "Команды: справка"
#: utils/evmenu.py:200
msgid "Choose an option or try 'help'."
msgstr "Выберите опцию или введите \"справка\"."
#: utils/utils.py:1866
#, python-format
msgid "Could not find '%s'."
msgstr "Не обнаружено '%s'."
#: utils/utils.py:1873
#, python-format
msgid ""
"More than one match for '%s' (please narrow target):\n"
msgstr ""
"Больше одного подходящего варианта для '%s' (уточните цель):\n"

View file

@ -246,7 +246,11 @@ class LockHandler(object):
evalstring = " ".join(_RE_OK.findall(evalstring)) evalstring = " ".join(_RE_OK.findall(evalstring))
eval(evalstring % tuple(True for func in funclist), {}, {}) eval(evalstring % tuple(True for func in funclist), {}, {})
except Exception: except Exception:
elist.append(_("Lock: definition '%s' has syntax errors.") % raw_lockstring) elist.append(
_("Lock: definition '{lock_string}' has syntax errors.").format(
lock_string=raw_lockstring
)
)
continue continue
if access_type in locks: if access_type in locks:
duplicates += 1 duplicates += 1

View file

@ -8,6 +8,7 @@ from django.contrib import admin
from evennia.typeclasses.admin import AttributeInline, TagInline from evennia.typeclasses.admin import AttributeInline, TagInline
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from django.contrib.admin.utils import flatten_fieldsets from django.contrib.admin.utils import flatten_fieldsets
from django.utils.translation import gettext as _
class ObjectAttributeInline(AttributeInline): class ObjectAttributeInline(AttributeInline):

View file

@ -159,7 +159,7 @@ class ObjectDBManager(TypedObjectManager):
typeclasses (list, optional): Python pats to restrict matches with. typeclasses (list, optional): Python pats to restrict matches with.
Returns: Returns:
matches (list): Objects fullfilling both the `attribute_name` and matches (query): Objects fullfilling both the `attribute_name` and
`attribute_value` criterions. `attribute_value` criterions.
Notes: Notes:
@ -273,7 +273,7 @@ class ObjectDBManager(TypedObjectManager):
to exclude from the match. to exclude from the match.
Returns: Returns:
contents (list): Matching contents, without excludeobj, if given. contents (query): Matching contents, without excludeobj, if given.
""" """
exclude_restriction = ( exclude_restriction = (
Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q() Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q()
@ -291,7 +291,7 @@ class ObjectDBManager(TypedObjectManager):
typeclasses (list): Only match objects with typeclasses having thess path strings. typeclasses (list): Only match objects with typeclasses having thess path strings.
Returns: Returns:
matches (list): A list of matches of length 0, 1 or more. matches (query): A list of matches of length 0, 1 or more.
""" """
if not isinstance(ostring, str): if not isinstance(ostring, str):
if hasattr(ostring, "key"): if hasattr(ostring, "key"):

View file

@ -12,7 +12,7 @@ from collections import defaultdict
from django.conf import settings from django.conf import settings
from evennia.typeclasses.models import TypeclassBase from evennia.typeclasses.models import TypeclassBase
from evennia.typeclasses.attributes import NickHandler from evennia.typeclasses.attributes import NickHandler, ModelAttributeBackend
from evennia.objects.manager import ObjectManager from evennia.objects.manager import ObjectManager
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.scripts.scripthandler import ScriptHandler from evennia.scripts.scripthandler import ScriptHandler
@ -225,7 +225,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
@lazy_property @lazy_property
def nicks(self): def nicks(self):
return NickHandler(self) return NickHandler(self, ModelAttributeBackend)
@lazy_property @lazy_property
def sessions(self): def sessions(self):
@ -503,7 +503,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
) )
if quiet: if quiet:
return results return list(results)
return _AT_SEARCH_RESULT( return _AT_SEARCH_RESULT(
results, results,
self, self,
@ -1059,7 +1059,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# See if we need to kick the account off. # See if we need to kick the account off.
for session in self.sessions.all(): for session in self.sessions.all():
session.msg(_("Your character %s has been destroyed.") % self.key) session.msg(_("Your character {key} has been destroyed.").format(key=self.key))
# no need to disconnect, Account just jumps to OOC mode. # no need to disconnect, Account just jumps to OOC mode.
# sever the connection (important!) # sever the connection (important!)
if self.account: if self.account:

View file

@ -2262,7 +2262,7 @@ def main():
if option in ("makemessages", "compilemessages"): if option in ("makemessages", "compilemessages"):
# some commands don't require the presence of a game directory to work # some commands don't require the presence of a game directory to work
need_gamedir = False need_gamedir = False
if option in ("shell", "check", "makemigrations"): if option in ("shell", "check", "makemigrations", "createsuperuser"):
# some django commands requires the database to exist, # some django commands requires the database to exist,
# or evennia._init to have run before they work right. # or evennia._init to have run before they work right.
check_db = True check_db = True

View file

@ -465,11 +465,16 @@ def getKeyPair(pubkeyfile, privkeyfile):
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)): if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
# No keypair exists. Generate a new RSA keypair # No keypair exists. Generate a new RSA keypair
from Crypto.PublicKey import RSA from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
rsa_key = Key(RSA.generate(_KEY_LENGTH)) rsa_key = Key(
public_key_string = rsa_key.public().toString(type="OPENSSH") rsa.generate_private_key(
private_key_string = rsa_key.toString(type="OPENSSH") public_exponent=65537, key_size=_KEY_LENGTH, backend=default_backend()
)
)
public_key_string = rsa_key.public().toString(type="OPENSSH").decode()
private_key_string = rsa_key.toString(type="OPENSSH").decode()
# save keys for the future. # save keys for the future.
with open(privkeyfile, "wt") as pfile: with open(privkeyfile, "wt") as pfile:

View file

@ -22,6 +22,7 @@ import django
django.setup() django.setup()
import evennia import evennia
import importlib
evennia._init() evennia._init()
@ -31,7 +32,6 @@ from django.conf import settings
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.scripts.models import ScriptDB from evennia.scripts.models import ScriptDB
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.server import initial_setup
from evennia.utils.utils import get_evennia_version, mod_import, make_iter from evennia.utils.utils import get_evennia_version, mod_import, make_iter
from evennia.utils import logger from evennia.utils import logger
@ -105,6 +105,7 @@ _IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
_GAMETIME_MODULE = None _GAMETIME_MODULE = None
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT _IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_LAST_SERVER_TIME_SNAPSHOT = 0
def _server_maintenance(): def _server_maintenance():
@ -113,6 +114,8 @@ def _server_maintenance():
the server needs to do. It is called every minute. the server needs to do. It is called every minute.
""" """
global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE
global _LAST_SERVER_TIME_SNAPSHOT
if not _FLUSH_CACHE: if not _FLUSH_CACHE:
from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE
if not _GAMETIME_MODULE: if not _GAMETIME_MODULE:
@ -125,8 +128,13 @@ def _server_maintenance():
# first call after a reload # first call after a reload
_GAMETIME_MODULE.SERVER_START_TIME = now _GAMETIME_MODULE.SERVER_START_TIME = now
_GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0) _GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0)
_LAST_SERVER_TIME_SNAPSHOT = now
else: else:
_GAMETIME_MODULE.SERVER_RUNTIME += 60.0 # adjust the runtime not with 60s but with the actual elapsed time
# in case this may varies slightly from 60s.
_GAMETIME_MODULE.SERVER_RUNTIME += (now - _LAST_SERVER_TIME_SNAPSHOT)
_LAST_SERVER_TIME_SNAPSHOT = now
# update game time and save it across reloads # update game time and save it across reloads
_GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now _GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.SERVER_RUNTIME) ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.SERVER_RUNTIME)
@ -333,6 +341,7 @@ class Evennia(object):
Once finished the last_initial_setup_step is set to -1. Once finished the last_initial_setup_step is set to -1.
""" """
global INFO_DICT global INFO_DICT
initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step") last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step")
if not last_initial_setup_step: if not last_initial_setup_step:
# None is only returned if the config does not exist, # None is only returned if the config does not exist,

View file

@ -6,7 +6,6 @@ connection actually happens (so it's the same for telnet, web, ssh etc).
It is stored on the Server side (as opposed to protocol-specific sessions which It is stored on the Server side (as opposed to protocol-specific sessions which
are stored on the Portal side) are stored on the Portal side)
""" """
import weakref
import time import time
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
@ -16,6 +15,7 @@ from evennia.utils.utils import make_iter, lazy_property
from evennia.commands.cmdsethandler import CmdSetHandler from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.server.session import Session from evennia.server.session import Session
from evennia.scripts.monitorhandler import MONITOR_HANDLER from evennia.scripts.monitorhandler import MONITOR_HANDLER
from evennia.typeclasses.attributes import AttributeHandler, InMemoryAttributeBackend, DbHolder
_GA = object.__getattribute__ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
@ -25,124 +25,6 @@ _ANSI = None
# i18n # i18n
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
# Handlers for Session.db/ndb operation
class NDbHolder(object):
"""Holder for allowing property access of attributes"""
def __init__(self, obj, name, manager_name="attributes"):
_SA(self, name, _GA(obj, manager_name))
_SA(self, "name", name)
def __getattribute__(self, attrname):
if attrname == "all":
# we allow to overload our default .all
attr = _GA(self, _GA(self, "name")).get("all")
return attr if attr else _GA(self, "all")
return _GA(self, _GA(self, "name")).get(attrname)
def __setattr__(self, attrname, value):
_GA(self, _GA(self, "name")).add(attrname, value)
def __delattr__(self, attrname):
_GA(self, _GA(self, "name")).remove(attrname)
def get_all(self):
return _GA(self, _GA(self, "name")).all()
all = property(get_all)
class NAttributeHandler(object):
"""
NAttributeHandler version without recache protection.
This stand-alone handler manages non-database saving.
It is similar to `AttributeHandler` and is used
by the `.ndb` handler in the same way as `.db` does
for the `AttributeHandler`.
"""
def __init__(self, obj):
"""
Initialized on the object
"""
self._store = {}
self.obj = weakref.proxy(obj)
def has(self, key):
"""
Check if object has this attribute or not.
Args:
key (str): The Nattribute key to check.
Returns:
has_nattribute (bool): If Nattribute is set or not.
"""
return key in self._store
def get(self, key, default=None):
"""
Get the named key value.
Args:
key (str): The Nattribute key to get.
Returns:
the value of the Nattribute.
"""
return self._store.get(key, default)
def add(self, key, value):
"""
Add new key and value.
Args:
key (str): The name of Nattribute to add.
value (any): The value to store.
"""
self._store[key] = value
def remove(self, key):
"""
Remove Nattribute from storage.
Args:
key (str): The name of the Nattribute to remove.
"""
if key in self._store:
del self._store[key]
def clear(self):
"""
Remove all NAttributes from handler.
"""
self._store = {}
def all(self, return_tuples=False):
"""
List the contents of the handler.
Args:
return_tuples (bool, optional): Defines if the Nattributes
are returns as a list of keys or as a list of `(key, value)`.
Returns:
nattributes (list): A list of keys `[key, key, ...]` or a
list of tuples `[(key, value), ...]` depending on the
setting of `return_tuples`.
"""
if return_tuples:
return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")]
return [key for key in self._store if not key.startswith("_")]
# ------------------------------------------------------------- # -------------------------------------------------------------
# Server Session # Server Session
@ -175,6 +57,10 @@ class ServerSession(Session):
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set) cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
@property
def id(self):
return self.sessid
def at_sync(self): def at_sync(self):
""" """
This is called whenever a session has been resynced with the This is called whenever a session has been resynced with the
@ -490,7 +376,7 @@ class ServerSession(Session):
@lazy_property @lazy_property
def nattributes(self): def nattributes(self):
return NAttributeHandler(self) return AttributeHandler(self, InMemoryAttributeBackend)
@lazy_property @lazy_property
def attributes(self): def attributes(self):
@ -508,7 +394,7 @@ class ServerSession(Session):
try: try:
return self._ndb_holder return self._ndb_holder
except AttributeError: except AttributeError:
self._ndb_holder = NDbHolder(self, "nattrhandler", manager_name="nattributes") self._ndb_holder = DbHolder(self, "nattrhandler", manager_name="nattributes")
return self._ndb_holder return self._ndb_holder
# @ndb.setter # @ndb.setter

View file

@ -101,7 +101,9 @@ class Session(object):
the keys given by self._attrs_to_sync. the keys given by self._attrs_to_sync.
""" """
return {attr: getattr(self, attr, None) for attr in settings.SESSION_SYNC_ATTRS} return {
attr: getattr(self, attr) for attr in settings.SESSION_SYNC_ATTRS if hasattr(self, attr)
}
def load_sync_data(self, sessdata): def load_sync_data(self, sessdata):
""" """

View file

@ -66,6 +66,7 @@ class TestServer(TestCase):
connection=DEFAULT, connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000, _IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3600 - 1, _MAINTENANCE_COUNT=3600 - 1,
_LAST_SERVER_TIME_SNAPSHOT=0,
ServerConfig=DEFAULT, ServerConfig=DEFAULT,
) as mocks: ) as mocks:
mocks["connection"].close = MagicMock() mocks["connection"].close = MagicMock()
@ -84,6 +85,7 @@ class TestServer(TestCase):
connection=DEFAULT, connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000, _IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3700 - 1, _MAINTENANCE_COUNT=3700 - 1,
_LAST_SERVER_TIME_SNAPSHOT=0,
ServerConfig=DEFAULT, ServerConfig=DEFAULT,
) as mocks: ) as mocks:
mocks["connection"].close = MagicMock() mocks["connection"].close = MagicMock()
@ -101,6 +103,7 @@ class TestServer(TestCase):
connection=DEFAULT, connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000, _IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1, _MAINTENANCE_COUNT=(3600 * 7) - 1,
_LAST_SERVER_TIME_SNAPSHOT=0,
ServerConfig=DEFAULT, ServerConfig=DEFAULT,
) as mocks: ) as mocks:
mocks["connection"].close = MagicMock() mocks["connection"].close = MagicMock()
@ -117,6 +120,7 @@ class TestServer(TestCase):
connection=DEFAULT, connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000, _IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1, _MAINTENANCE_COUNT=(3600 * 7) - 1,
_LAST_SERVER_TIME_SNAPSHOT=0,
SESSIONS=DEFAULT, SESSIONS=DEFAULT,
_IDLE_TIMEOUT=10, _IDLE_TIMEOUT=10,
time=DEFAULT, time=DEFAULT,

View file

@ -4,7 +4,7 @@ Master configuration file for Evennia.
NOTE: NO MODIFICATIONS SHOULD BE MADE TO THIS FILE! NOTE: NO MODIFICATIONS SHOULD BE MADE TO THIS FILE!
All settings changes should be done by copy-pasting the variable and All settings changes should be done by copy-pasting the variable and
its value to <gamedir>/conf/settings.py. its value to <gamedir>/server/conf/settings.py.
Hint: Don't copy&paste over more from this file than you actually want Hint: Don't copy&paste over more from this file than you actually want
to change. Anything you don't copy&paste will thus retain its default to change. Anything you don't copy&paste will thus retain its default
@ -332,6 +332,10 @@ CONNECTION_SCREEN_MODULE = "server.conf.connection_screens"
# cause issues with menu-logins and autoconnects since the menu will not have # cause issues with menu-logins and autoconnects since the menu will not have
# started when the autoconnects starts sending menu commands. # started when the autoconnects starts sending menu commands.
DELAY_CMD_LOGINSTART = 0.3 DELAY_CMD_LOGINSTART = 0.3
# A module that must exist - this holds the instructions Evennia will use to
# first prepare the database for use. Generally should not be changed. If this
# cannot be imported, bad things will happen.
INITIAL_SETUP_MODULE = "evennia.server.initial_setup"
# An optional module that, if existing, must hold a function # An optional module that, if existing, must hold a function
# named at_initial_setup(). This hook method can be used to customize # named at_initial_setup(). This hook method can be used to customize
# the server's initial setup sequence (the very first startup of the system). # the server's initial setup sequence (the very first startup of the system).

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
# Attribute manager methods # Attribute manager methods
def get_attribute( def get_attribute(
self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None, **kwargs
): ):
""" """
Return Attribute objects by key, by category, by value, by Return Attribute objects by key, by category, by value, by
@ -55,6 +55,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
attrype (str, optional): An attribute-type to search for. attrype (str, optional): An attribute-type to search for.
By default this is either `None` (normal Attributes) or By default this is either `None` (normal Attributes) or
`"nick"`. `"nick"`.
kwargs (any): Currently unused. Reserved for future use.
Returns: Returns:
attributes (list): The matching Attributes. attributes (list): The matching Attributes.
@ -102,7 +103,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
key=key, category=category, value=value, strvalue=strvalue, obj=obj key=key, category=category, value=value, strvalue=strvalue, obj=obj
) )
def get_by_attribute(self, key=None, category=None, value=None, strvalue=None, attrtype=None): def get_by_attribute(
self, key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
""" """
Return objects having attributes with the given key, category, Return objects having attributes with the given key, category,
value, strvalue or combination of those criteria. value, strvalue or combination of those criteria.
@ -122,6 +125,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
attrype (str, optional): An attribute-type to search for. attrype (str, optional): An attribute-type to search for.
By default this is either `None` (normal Attributes) or By default this is either `None` (normal Attributes) or
`"nick"`. `"nick"`.
kwargs (any): Currently unused. Reserved for future use.
Returns: Returns:
obj (list): Objects having the matching Attributes. obj (list): Objects having the matching Attributes.

View file

@ -36,7 +36,8 @@ from django.urls import reverse
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.text import slugify from django.utils.text import slugify
from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler from evennia.typeclasses.attributes import Attribute, AttributeHandler, ModelAttributeBackend, InMemoryAttributeBackend
from evennia.typeclasses.attributes import DbHolder
from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler
from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase
@ -121,33 +122,6 @@ class TypeclassBase(SharedMemoryModelBase):
signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class) signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class)
return new_class return new_class
class DbHolder(object):
"Holder for allowing property access of attributes"
def __init__(self, obj, name, manager_name="attributes"):
_SA(self, name, _GA(obj, manager_name))
_SA(self, "name", name)
def __getattribute__(self, attrname):
if attrname == "all":
# we allow to overload our default .all
attr = _GA(self, _GA(self, "name")).get("all")
return attr if attr else _GA(self, "all")
return _GA(self, _GA(self, "name")).get(attrname)
def __setattr__(self, attrname, value):
_GA(self, _GA(self, "name")).add(attrname, value)
def __delattr__(self, attrname):
_GA(self, _GA(self, "name")).remove(attrname)
def get_all(self):
return _GA(self, _GA(self, "name")).all()
all = property(get_all)
# #
# Main TypedObject abstraction # Main TypedObject abstraction
# #
@ -301,7 +275,7 @@ class TypedObject(SharedMemoryModel):
# initialize all handlers in a lazy fashion # initialize all handlers in a lazy fashion
@lazy_property @lazy_property
def attributes(self): def attributes(self):
return AttributeHandler(self) return AttributeHandler(self, ModelAttributeBackend)
@lazy_property @lazy_property
def locks(self): def locks(self):
@ -321,7 +295,7 @@ class TypedObject(SharedMemoryModel):
@lazy_property @lazy_property
def nattributes(self): def nattributes(self):
return NAttributeHandler(self) return AttributeHandler(self, InMemoryAttributeBackend)
class Meta(object): class Meta(object):
""" """

View file

@ -26,12 +26,12 @@ class TestAttributes(EvenniaTest):
key = "testattr" key = "testattr"
value = "test attr value " value = "test attr value "
self.obj1.attributes.add(key, value) self.obj1.attributes.add(key, value)
self.assertFalse(self.obj1.attributes._cache) self.assertFalse(self.obj1.attributes.backend._cache)
self.assertEqual(self.obj1.attributes.get(key), value) self.assertEqual(self.obj1.attributes.get(key), value)
self.obj1.db.testattr = value self.obj1.db.testattr = value
self.assertEqual(self.obj1.db.testattr, value) self.assertEqual(self.obj1.db.testattr, value)
self.assertFalse(self.obj1.attributes._cache) self.assertFalse(self.obj1.attributes.backend._cache)
def test_weird_text_save(self): def test_weird_text_save(self):
"test 'weird' text type (different in py2 vs py3)" "test 'weird' text type (different in py2 vs py3)"

View file

@ -501,6 +501,8 @@ def create_account(
report_to (Object): An object with a msg() method to report report_to (Object): An object with a msg() method to report
errors to. If not given, errors will be logged. errors to. If not given, errors will be logged.
Returns:
Account: The newly created Account.
Raises: Raises:
ValueError: If `key` already exists in database. ValueError: If `key` already exists in database.

View file

@ -205,27 +205,35 @@ help_entries = search_help
# not the attribute object itself (this is usually what you want) # not the attribute object itself (this is usually what you want)
def search_object_attribute(key=None, category=None, value=None, strvalue=None): def search_object_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
return ObjectDB.objects.get_by_attribute( return ObjectDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
) )
def search_account_attribute(key=None, category=None, value=None, strvalue=None): def search_account_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
return AccountDB.objects.get_by_attribute( return AccountDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
) )
def search_script_attribute(key=None, category=None, value=None, strvalue=None): def search_script_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
return ScriptDB.objects.get_by_attribute( return ScriptDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
) )
def search_channel_attribute(key=None, category=None, value=None, strvalue=None): def search_channel_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
return Channel.objects.get_by_attribute( return Channel.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
) )
@ -243,7 +251,7 @@ search_attribute_object = ObjectDB.objects.get_attribute
# object itself (this is usually what you want) # object itself (this is usually what you want)
def search_object_by_tag(key=None, category=None): def search_object_by_tag(key=None, category=None, tagtype=None, **kwargs):
""" """
Find object based on tag or category. Find object based on tag or category.
@ -252,6 +260,11 @@ def search_object_by_tag(key=None, category=None):
category (str, optional): The category of tag category (str, optional): The category of tag
to search for. If not set, uncategorized to search for. If not set, uncategorized
tags will be searched. tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns: Returns:
matches (list): List of Objects with tags matching matches (list): List of Objects with tags matching
@ -259,13 +272,13 @@ def search_object_by_tag(key=None, category=None):
matches were found. matches were found.
""" """
return ObjectDB.objects.get_by_tag(key=key, category=category) return ObjectDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
search_tag = search_object_by_tag # this is the most common case search_tag = search_object_by_tag # this is the most common case
def search_account_tag(key=None, category=None): def search_account_tag(key=None, category=None, tagtype=None, **kwargs):
""" """
Find account based on tag or category. Find account based on tag or category.
@ -274,6 +287,11 @@ def search_account_tag(key=None, category=None):
category (str, optional): The category of tag category (str, optional): The category of tag
to search for. If not set, uncategorized to search for. If not set, uncategorized
tags will be searched. tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns: Returns:
matches (list): List of Accounts with tags matching matches (list): List of Accounts with tags matching
@ -281,10 +299,10 @@ def search_account_tag(key=None, category=None):
matches were found. matches were found.
""" """
return AccountDB.objects.get_by_tag(key=key, category=category) return AccountDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
def search_script_tag(key=None, category=None): def search_script_tag(key=None, category=None, tagtype=None, **kwargs):
""" """
Find script based on tag or category. Find script based on tag or category.
@ -293,6 +311,11 @@ def search_script_tag(key=None, category=None):
category (str, optional): The category of tag category (str, optional): The category of tag
to search for. If not set, uncategorized to search for. If not set, uncategorized
tags will be searched. tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns: Returns:
matches (list): List of Scripts with tags matching matches (list): List of Scripts with tags matching
@ -300,10 +323,10 @@ def search_script_tag(key=None, category=None):
matches were found. matches were found.
""" """
return ScriptDB.objects.get_by_tag(key=key, category=category) return ScriptDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
def search_channel_tag(key=None, category=None): def search_channel_tag(key=None, category=None, tagtype=None, **kwargs):
""" """
Find channel based on tag or category. Find channel based on tag or category.
@ -312,6 +335,11 @@ def search_channel_tag(key=None, category=None):
category (str, optional): The category of tag category (str, optional): The category of tag
to search for. If not set, uncategorized to search for. If not set, uncategorized
tags will be searched. tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns: Returns:
matches (list): List of Channels with tags matching matches (list): List of Channels with tags matching
@ -319,7 +347,7 @@ def search_channel_tag(key=None, category=None):
matches were found. matches were found.
""" """
return Channel.objects.get_by_tag(key=key, category=category) return Channel.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
# search for tag objects (not the objects they are attached to # search for tag objects (not the objects they are attached to

View file

@ -157,3 +157,15 @@ class EvenniaTest(TestCase):
self.account.delete() self.account.delete()
self.account2.delete() self.account2.delete()
super().tearDown() super().tearDown()
class LocalEvenniaTest(EvenniaTest):
"""
This test class is intended for inheriting in mygame tests.
It helps ensure your tests are run with your own objects.
"""
account_typeclass = settings.BASE_ACCOUNT_TYPECLASS
object_typeclass = settings.BASE_OBJECT_TYPECLASS
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
exit_typeclass = settings.BASE_EXIT_TYPECLASS
room_typeclass = settings.BASE_ROOM_TYPECLASS
script_typeclass = settings.BASE_SCRIPT_TYPECLASS

View file

@ -2101,7 +2101,9 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
if multimatch_string: if multimatch_string:
error = "%s\n" % multimatch_string error = "%s\n" % multimatch_string
else: else:
error = _("More than one match for '%s' (please narrow target):\n" % query) error = _("More than one match for '{query}' (please narrow target):\n").format(
query=query
)
for num, result in enumerate(matches): for num, result in enumerate(matches):
# we need to consider Commands, where .aliases is a list # we need to consider Commands, where .aliases is a list

View file

@ -15,6 +15,7 @@ from django.core.exceptions import ValidationError as _error
from django.core.validators import validate_email as _val_email from django.core.validators import validate_email as _val_email
from evennia.utils.ansi import strip_ansi from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import string_partial_matching as _partial from evennia.utils.utils import string_partial_matching as _partial
from django.utils.translation import gettext as _
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones} _TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
@ -58,7 +59,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs)
""" """
if not entry: if not entry:
raise ValueError(f"No {option_key} entered!") raise ValueError(_("No {option_key} entered!").format(option_key=option_key))
if not from_tz: if not from_tz:
from_tz = _pytz.UTC from_tz = _pytz.UTC
if account: if account:
@ -66,7 +67,11 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs)
try: try:
from_tz = _pytz.timezone(acct_tz) from_tz = _pytz.timezone(acct_tz)
except Exception as err: except Exception as err:
raise ValueError(f"Timezone string '{acct_tz}' is not a valid timezone ({err})") raise ValueError(
_("Timezone string '{acct_tz}' is not a valid timezone ({err})").format(
acct_tz=acct_tz, err=err
)
)
else: else:
from_tz = _pytz.UTC from_tz = _pytz.UTC