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)
- Made most of the networking classes such as Protocols and the SessionHandlers
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.
### Already in master
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `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_UNPUPPET,
)
from evennia.typeclasses.attributes import NickHandler
from evennia.typeclasses.attributes import NickHandler, ModelAttributeBackend
from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.utils.optionhandler import OptionHandler
@ -199,7 +199,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
@lazy_property
def nicks(self):
return NickHandler(self)
return NickHandler(self, ModelAttributeBackend)
@lazy_property
def sessions(self):
@ -275,11 +275,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
raise RuntimeError("Session not found")
if self.get_puppet(session) == obj:
# already puppeting this object
self.msg("You are already puppeting this object.")
self.msg(_("You are already puppeting this object."))
return
if not obj.access(self, "puppet"):
# 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
if obj.account:
# object already puppeted
@ -295,12 +295,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
else:
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"
self.msg(txt1, session=session)
self.msg(txt2, session=obj.sessions.all())
self.msg(_(txt1), session=session)
self.msg(_(txt2), session=obj.sessions.all())
self.unpuppet_object(obj.sessions.get())
elif obj.account.is_connected:
# 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
# do the puppeting
@ -496,7 +496,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# See if authentication is currently being throttled
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
# waste of storage and can be abused to make your logs harder to
@ -508,8 +508,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if banned:
# this is a banned IP or name!
errors.append(
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
_(
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
)
)
logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.")
@ -519,7 +521,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
account = authenticate(username=username, password=password)
if not account:
# 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
logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")
@ -688,7 +690,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
ip = kwargs.get("ip", "")
if ip and CREATION_THROTTLE.check(ip):
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
@ -716,7 +718,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
banned = cls.is_banned(username=username, ip=ip)
if banned:
# this is a banned IP or name!
string = (
string = _(
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
)
@ -733,7 +735,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
except Exception as e:
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()
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
# to handle tracebacks ourselves at this point. If we don't,
# 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()
# Update the throttle to indicate a new account was created from this IP
@ -1253,21 +1257,21 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if session:
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:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
self.msg("The Character does not exist.")
self.msg(_("The Character does not exist."))
return
elif _MULTISESSION_MODE == 1:
# in this mode all sessions connect to the same puppet.
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
self.msg("The Character does not exist.")
self.msg(_("The Character does not exist."))
return
elif _MULTISESSION_MODE in (2, 3):
# 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 ''})"
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):
"""
@ -1411,7 +1417,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if hasattr(target, "return_appearance"):
return target.return_appearance(self)
else:
return "{} has no in-game appearance.".format(target)
return _("{target} has no in-game appearance.").format(target=target)
else:
# list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else []
@ -1454,7 +1460,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if is_su or len(characters) < charmax:
if not characters:
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:
result.append("\n |w@charcreate <name> [=description]|n - create new character")
@ -1534,7 +1542,7 @@ class DefaultGuest(DefaultAccount):
# check if guests are 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
try:
@ -1544,7 +1552,7 @@ class DefaultGuest(DefaultAccount):
username = name
break
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:
LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.")
return None, errors
@ -1572,7 +1580,7 @@ class DefaultGuest(DefaultAccount):
# 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 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()
return None, errors
@ -1589,7 +1597,7 @@ class DefaultGuest(DefaultAccount):
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)
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.utils import search
from evennia.utils import utils
from django.utils.translation import gettext as _
_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})"
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
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 = []
return
@ -337,7 +340,11 @@ class IRCBot(Bot):
if hasattr(self, "_ping_callers") and self._ping_callers:
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
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 = []
return

View file

@ -743,7 +743,9 @@ def cmdhandler(
sysarg = raw_string
else:
# 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(
raw_string,
cmdset.get_all_cmd_keys_and_aliases(caller),
@ -751,8 +753,8 @@ def cmdhandler(
maxnum=3,
)
if suggestions:
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(
suggestions, _("or"), addquote=True
sysarg += _(" Maybe you meant {command}?").format(
command=utils.list_to_string(suggestions, _("or"), addquote=True)
)
else:
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)
else:
# try next suggested path
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
errstring += _("\n(Unsuccessfully tried '{path}').").format(
path=python_path
)
continue
try:
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()
raise exc.with_traceback(tb)
else:
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
errstring += _("\n(Unsuccessfully tried '{path}').").format(
path=python_path
)
continue
_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
unmuted = channel.unmute(caller)
if unmuted:
self.msg("You start listening to %s." % channel)
self.msg(_("You start listening to %s.") % channel)
return
self.msg("You were already listening to %s." % channel)
self.msg(_("You were already listening to %s.") % channel)
return
if msg == "off":
caller = caller if not hasattr(caller, "account") else caller.account
muted = channel.mute(caller)
if muted:
self.msg("You stop listening to %s." % channel)
self.msg(_("You stop listening to %s.") % channel)
return
self.msg("You were already not listening to %s." % channel)
self.msg(_("You were already not listening to %s.") % channel)
return
if self.history_start is not None:
# Try to view history
@ -144,7 +144,7 @@ class ChannelCommand(command.Command):
else:
caller = caller if not hasattr(caller, "account") else caller.account
if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel)
self.msg(_("You currently have %s muted.") % channel)
return
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_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.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])
@ -488,7 +488,7 @@ class CmdArmPuzzle(MuxCommand):
Notes:
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:
topic.help_category = default_category
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)
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))
eval(evalstring % tuple(True for func in funclist), {}, {})
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
if access_type in locks:
duplicates += 1

View file

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

View file

@ -159,7 +159,7 @@ class ObjectDBManager(TypedObjectManager):
typeclasses (list, optional): Python pats to restrict matches with.
Returns:
matches (list): Objects fullfilling both the `attribute_name` and
matches (query): Objects fullfilling both the `attribute_name` and
`attribute_value` criterions.
Notes:
@ -273,7 +273,7 @@ class ObjectDBManager(TypedObjectManager):
to exclude from the match.
Returns:
contents (list): Matching contents, without excludeobj, if given.
contents (query): Matching contents, without excludeobj, if given.
"""
exclude_restriction = (
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.
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 hasattr(ostring, "key"):

View file

@ -12,7 +12,7 @@ from collections import defaultdict
from django.conf import settings
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.models import ObjectDB
from evennia.scripts.scripthandler import ScriptHandler
@ -225,7 +225,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
@lazy_property
def nicks(self):
return NickHandler(self)
return NickHandler(self, ModelAttributeBackend)
@lazy_property
def sessions(self):
@ -503,7 +503,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
)
if quiet:
return results
return list(results)
return _AT_SEARCH_RESULT(
results,
self,
@ -1059,7 +1059,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# See if we need to kick the account off.
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.
# sever the connection (important!)
if self.account:

View file

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

View file

@ -465,11 +465,16 @@ def getKeyPair(pubkeyfile, privkeyfile):
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
# 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))
public_key_string = rsa_key.public().toString(type="OPENSSH")
private_key_string = rsa_key.toString(type="OPENSSH")
rsa_key = Key(
rsa.generate_private_key(
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.
with open(privkeyfile, "wt") as pfile:

View file

@ -22,6 +22,7 @@ import django
django.setup()
import evennia
import importlib
evennia._init()
@ -31,7 +32,6 @@ from django.conf import settings
from evennia.accounts.models import AccountDB
from evennia.scripts.models import ScriptDB
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 import logger
@ -105,6 +105,7 @@ _IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
_GAMETIME_MODULE = None
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_LAST_SERVER_TIME_SNAPSHOT = 0
def _server_maintenance():
@ -113,6 +114,8 @@ def _server_maintenance():
the server needs to do. It is called every minute.
"""
global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE
global _LAST_SERVER_TIME_SNAPSHOT
if not _FLUSH_CACHE:
from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE
if not _GAMETIME_MODULE:
@ -125,8 +128,13 @@ def _server_maintenance():
# first call after a reload
_GAMETIME_MODULE.SERVER_START_TIME = now
_GAMETIME_MODULE.SERVER_RUNTIME = ServerConfig.objects.conf("runtime", default=0.0)
_LAST_SERVER_TIME_SNAPSHOT = now
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
_GAMETIME_MODULE.SERVER_RUNTIME_LAST_UPDATED = now
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.
"""
global INFO_DICT
initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step")
if not last_initial_setup_step:
# 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
are stored on the Portal side)
"""
import weakref
import time
from django.utils import timezone
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.server.session import Session
from evennia.scripts.monitorhandler import MONITOR_HANDLER
from evennia.typeclasses.attributes import AttributeHandler, InMemoryAttributeBackend, DbHolder
_GA = object.__getattribute__
_SA = object.__setattr__
@ -25,124 +25,6 @@ _ANSI = None
# i18n
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
@ -175,6 +57,10 @@ class ServerSession(Session):
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
@property
def id(self):
return self.sessid
def at_sync(self):
"""
This is called whenever a session has been resynced with the
@ -490,7 +376,7 @@ class ServerSession(Session):
@lazy_property
def nattributes(self):
return NAttributeHandler(self)
return AttributeHandler(self, InMemoryAttributeBackend)
@lazy_property
def attributes(self):
@ -508,7 +394,7 @@ class ServerSession(Session):
try:
return self._ndb_holder
except AttributeError:
self._ndb_holder = NDbHolder(self, "nattrhandler", manager_name="nattributes")
self._ndb_holder = DbHolder(self, "nattrhandler", manager_name="nattributes")
return self._ndb_holder
# @ndb.setter

View file

@ -101,7 +101,9 @@ class Session(object):
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):
"""

View file

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

View file

@ -4,7 +4,7 @@ Master configuration file for Evennia.
NOTE: NO MODIFICATIONS SHOULD BE MADE TO THIS FILE!
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
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
# started when the autoconnects starts sending menu commands.
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
# 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).

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
# Attribute manager methods
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
@ -55,6 +55,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
attrype (str, optional): An attribute-type to search for.
By default this is either `None` (normal Attributes) or
`"nick"`.
kwargs (any): Currently unused. Reserved for future use.
Returns:
attributes (list): The matching Attributes.
@ -102,7 +103,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
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,
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.
By default this is either `None` (normal Attributes) or
`"nick"`.
kwargs (any): Currently unused. Reserved for future use.
Returns:
obj (list): Objects having the matching Attributes.
@ -488,12 +492,12 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
def get_typeclass_totals(self, *args, **kwargs) -> object:
"""
Returns a queryset of typeclass composition statistics.
Returns:
qs (Queryset): A queryset of dicts containing the typeclass (name),
qs (Queryset): A queryset of dicts containing the typeclass (name),
the count of objects with that typeclass and a float representing
the percentage of objects associated with the typeclass.
"""
return (
self.values("db_typeclass_path")

View file

@ -36,7 +36,8 @@ from django.urls import reverse
from django.utils.encoding import smart_str
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.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase
@ -121,33 +122,6 @@ class TypeclassBase(SharedMemoryModelBase):
signals.pre_delete.connect(remove_attributes_on_delete, sender=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
#
@ -301,7 +275,7 @@ class TypedObject(SharedMemoryModel):
# initialize all handlers in a lazy fashion
@lazy_property
def attributes(self):
return AttributeHandler(self)
return AttributeHandler(self, ModelAttributeBackend)
@lazy_property
def locks(self):
@ -321,7 +295,7 @@ class TypedObject(SharedMemoryModel):
@lazy_property
def nattributes(self):
return NAttributeHandler(self)
return AttributeHandler(self, InMemoryAttributeBackend)
class Meta(object):
"""

View file

@ -26,12 +26,12 @@ class TestAttributes(EvenniaTest):
key = "testattr"
value = "test attr 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.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):
"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
errors to. If not given, errors will be logged.
Returns:
Account: The newly created Account.
Raises:
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)
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(
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(
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(
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(
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)
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.
@ -252,6 +260,11 @@ def search_object_by_tag(key=None, category=None):
category (str, optional): The category of tag
to search for. If not set, uncategorized
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:
matches (list): List of Objects with tags matching
@ -259,13 +272,13 @@ def search_object_by_tag(key=None, category=None):
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
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.
@ -274,6 +287,11 @@ def search_account_tag(key=None, category=None):
category (str, optional): The category of tag
to search for. If not set, uncategorized
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:
matches (list): List of Accounts with tags matching
@ -281,10 +299,10 @@ def search_account_tag(key=None, category=None):
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.
@ -293,6 +311,11 @@ def search_script_tag(key=None, category=None):
category (str, optional): The category of tag
to search for. If not set, uncategorized
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:
matches (list): List of Scripts with tags matching
@ -300,10 +323,10 @@ def search_script_tag(key=None, category=None):
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.
@ -312,6 +335,11 @@ def search_channel_tag(key=None, category=None):
category (str, optional): The category of tag
to search for. If not set, uncategorized
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:
matches (list): List of Channels with tags matching
@ -319,7 +347,7 @@ def search_channel_tag(key=None, category=None):
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

View file

@ -157,3 +157,15 @@ class EvenniaTest(TestCase):
self.account.delete()
self.account2.delete()
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:
error = "%s\n" % multimatch_string
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):
# 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 evennia.utils.ansi import strip_ansi
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}
@ -58,7 +59,7 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs)
"""
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:
from_tz = _pytz.UTC
if account:
@ -66,7 +67,11 @@ def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs)
try:
from_tz = _pytz.timezone(acct_tz)
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:
from_tz = _pytz.UTC