mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 09:16:32 +01:00
Resolve merge conflicts, some cleanup
This commit is contained in:
commit
b024c17f8a
15 changed files with 854 additions and 29 deletions
|
|
@ -126,6 +126,9 @@ Web/Django standard initiative (@strikaco)
|
|||
- `evennia.MONITOR_HANDLER.all` now takes keyword argument `obj` to only retrieve monitors from that specific
|
||||
Object (rather than all monitors in the entire handler).
|
||||
- Support adding `\f` in command doc strings to force where EvMore puts page breaks.
|
||||
- Validation Functions now added with standard API to homogenize user input validation.
|
||||
- Option Classes added to make storing user-options easier and smoother.
|
||||
- `evennia.VALIDATOR_CONTAINER` and `evennia.OPTION_CONTAINER` added to load these.
|
||||
|
||||
### Contribs
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ MONITOR_HANDLER = None
|
|||
CHANNEL_HANDLER = None
|
||||
GLOBAL_SCRIPTS = None
|
||||
|
||||
# Containers
|
||||
VALIDATOR_FUNCTIONS = None
|
||||
OPTION_CLASSES = None
|
||||
|
||||
|
||||
def _create_version():
|
||||
"""
|
||||
|
|
@ -140,6 +144,7 @@ def _create_version():
|
|||
__version__ = _create_version()
|
||||
del _create_version
|
||||
|
||||
|
||||
def _init():
|
||||
"""
|
||||
This function is called automatically by the launcher only after
|
||||
|
|
@ -150,12 +155,16 @@ def _init():
|
|||
global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
|
||||
global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
|
||||
global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand
|
||||
global search_object, search_script, search_account, search_channel, search_help, search_tag, search_message
|
||||
global create_object, create_script, create_account, create_channel, create_message, create_help_entry
|
||||
global search_object, search_script, search_account, search_channel
|
||||
global search_help, search_tag, search_message
|
||||
global create_object, create_script, create_account, create_channel
|
||||
global create_message, create_help_entry
|
||||
global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers
|
||||
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER, GLOBAL_SCRIPTS
|
||||
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER
|
||||
global CHANNEL_HANDLER, TASK_HANDLER, GLOBAL_SCRIPTS
|
||||
global VALIDATOR_FUNCS, OPTION_CLASSES
|
||||
global EvMenu, EvTable, EvForm, EvMore, EvEditor
|
||||
global ANSIString
|
||||
global ANSIString
|
||||
|
||||
from .accounts.accounts import DefaultAccount
|
||||
from .accounts.accounts import DefaultGuest
|
||||
|
|
@ -216,6 +225,10 @@ def _init():
|
|||
from .scripts.monitorhandler import MONITOR_HANDLER
|
||||
from .utils.containers import GLOBAL_SCRIPTS
|
||||
|
||||
# containers
|
||||
from .utils.containers import VALIDATOR_FUNCS
|
||||
from .utils.containers import OPTION_CLASSES
|
||||
|
||||
# initialize the doc string
|
||||
global __doc__
|
||||
__doc__ = ansi.parse_ansi(DOCSTRING)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from django.utils.module_loading import import_string
|
|||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.accounts.manager import AccountManager
|
||||
from evennia.accounts.models import AccountDB
|
||||
from evennia.utils.option import OptionHandler
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.commands import cmdhandler
|
||||
|
|
@ -197,6 +198,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
def sessions(self):
|
||||
return AccountSessionHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def options(self):
|
||||
return OptionHandler(self, options_dict=settings.OPTIONS_ACCOUNT_DEFAULT, save_category='option')
|
||||
|
||||
# Do not make this a lazy property; the web UI will not refresh it!
|
||||
@property
|
||||
def characters(self):
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ All commands in Evennia inherit from the 'Command' class in this module.
|
|||
from builtins import range
|
||||
|
||||
import re
|
||||
import math
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.utils.utils import is_iter, fill, lazy_property, make_iter
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.ansi import ANSIString
|
||||
|
||||
from future.utils import with_metaclass
|
||||
|
||||
|
||||
|
|
@ -468,6 +472,98 @@ class Command(with_metaclass(CommandMeta, object)):
|
|||
"""
|
||||
return self.__doc__
|
||||
|
||||
def width(self):
|
||||
return self.session.protocol_flags['SCREENWIDTH'][0]
|
||||
|
||||
def style_table(self, *args, **kwargs):
|
||||
border_color = self.account.options.get('border_color')
|
||||
column_color = self.account.options.get('column_names_color')
|
||||
|
||||
colornames = ['|%s%s|n' % (column_color, col) for col in args]
|
||||
|
||||
h_line_char = kwargs.pop('header_line_char', '~')
|
||||
header_line_char = ANSIString(f'|{border_color}{h_line_char}|n')
|
||||
|
||||
c_char = kwargs.pop('corner_char', '+')
|
||||
corner_char = ANSIString(f'|{border_color}{c_char}|n')
|
||||
|
||||
b_left_char = kwargs.pop('border_left_char', '||')
|
||||
border_left_char = ANSIString(f'|{border_color}{b_left_char}|n')
|
||||
|
||||
b_right_char = kwargs.pop('border_right_char', '||')
|
||||
border_right_char = ANSIString(f'|{border_color}{b_right_char}|n')
|
||||
|
||||
b_bottom_char = kwargs.pop('border_bottom_char', '-')
|
||||
border_bottom_char = ANSIString(f'|{border_color}{b_bottom_char}|n')
|
||||
|
||||
b_top_char = kwargs.pop('border_top_char', '-')
|
||||
border_top_char = ANSIString(f'|{border_color}{b_top_char}|n')
|
||||
|
||||
table = EvTable(*colornames, header_line_char=header_line_char, corner_char=corner_char,
|
||||
border_left_char=border_left_char, border_right_char=border_right_char,
|
||||
border_top_char=border_top_char, **kwargs)
|
||||
return table
|
||||
|
||||
def render_header(self, header_text=None, fill_character=None, edge_character=None,
|
||||
mode='header', color_header=True):
|
||||
colors = dict()
|
||||
colors['border'] = self.account.options.get('border_color')
|
||||
colors['headertext'] = self.account.options.get('%s_text_color' % mode)
|
||||
colors['headerstar'] = self.account.options.get('%s_star_color' % mode)
|
||||
|
||||
width = self.width()
|
||||
if edge_character:
|
||||
width -= 2
|
||||
|
||||
if header_text:
|
||||
if color_header:
|
||||
header_text = ANSIString(header_text).clean()
|
||||
header_text = ANSIString('|n|%s%s|n' % (colors['headertext'], header_text))
|
||||
if mode == 'header':
|
||||
begin_center = ANSIString("|n|%s<|%s* |n" % (colors['border'], colors['headerstar']))
|
||||
end_center = ANSIString("|n |%s*|%s>|n" % (colors['headerstar'], colors['border']))
|
||||
center_string = ANSIString(begin_center + header_text + end_center)
|
||||
else:
|
||||
center_string = ANSIString('|n |%s%s |n' % (colors['headertext'], header_text))
|
||||
else:
|
||||
center_string = ''
|
||||
|
||||
fill_character = self.account.options.get('%s_fill' % mode)
|
||||
|
||||
remain_fill = width - len(center_string)
|
||||
if remain_fill % 2 == 0:
|
||||
right_width = remain_fill / 2
|
||||
left_width = remain_fill / 2
|
||||
else:
|
||||
right_width = math.floor(remain_fill / 2)
|
||||
left_width = math.ceil(remain_fill / 2)
|
||||
|
||||
right_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(right_width)))
|
||||
left_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(left_width)))
|
||||
|
||||
if edge_character:
|
||||
edge_fill = ANSIString('|n|%s%s|n' % (colors['border'], edge_character))
|
||||
main_string = ANSIString(center_string)
|
||||
final_send = ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill)
|
||||
else:
|
||||
final_send = left_fill + ANSIString(center_string) + right_fill
|
||||
return final_send
|
||||
|
||||
def style_header(self, *args, **kwargs):
|
||||
if 'mode' not in kwargs:
|
||||
kwargs['mode'] = 'header'
|
||||
return self.render_header(*args, **kwargs)
|
||||
|
||||
def style_separator(self, *args, **kwargs):
|
||||
if 'mode' not in kwargs:
|
||||
kwargs['mode'] = 'separator'
|
||||
return self.render_header(*args, **kwargs)
|
||||
|
||||
def style_footer(self, *args, **kwargs):
|
||||
if 'mode' not in kwargs:
|
||||
kwargs['mode'] = 'footer'
|
||||
return self.render_header(*args, **kwargs)
|
||||
|
||||
|
||||
class InterruptCommand(Exception):
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
|
|||
"""Implement function"""
|
||||
account = self.account
|
||||
sessions = account.sessions.all()
|
||||
table = evtable.EvTable("|wsessid",
|
||||
table = self.style_table("|wsessid",
|
||||
"|wprotocol",
|
||||
"|whost",
|
||||
"|wpuppet/character",
|
||||
|
|
@ -418,7 +418,7 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
|
|||
naccounts = (SESSIONS.account_count())
|
||||
if show_session_data:
|
||||
# privileged info
|
||||
table = evtable.EvTable("|wAccount Name",
|
||||
table = self.style_table("|wAccount Name",
|
||||
"|wOn for",
|
||||
"|wIdle",
|
||||
"|wPuppeting",
|
||||
|
|
@ -444,7 +444,7 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
|
|||
isinstance(session.address, tuple) and session.address[0] or session.address)
|
||||
else:
|
||||
# unprivileged
|
||||
table = evtable.EvTable("|wAccount name", "|wOn for", "|wIdle")
|
||||
table = self.style_table("|wAccount name", "|wOn for", "|wIdle")
|
||||
for session in session_list:
|
||||
if not session.logged_in:
|
||||
continue
|
||||
|
|
@ -524,7 +524,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
options.pop("TTYPE", None)
|
||||
|
||||
header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value")
|
||||
table = evtable.EvTable(*header)
|
||||
table = self.style_table(*header)
|
||||
for key in sorted(options):
|
||||
row = [key, options[key]]
|
||||
if saved_options:
|
||||
|
|
@ -870,3 +870,30 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
self.msg("Quelling Account permissions%s. Use @unquell to get them back." % permstr)
|
||||
self._recache_locks(account)
|
||||
|
||||
|
||||
class CmdStyle(COMMAND_DEFAULT_CLASS):
|
||||
key = "@style"
|
||||
switch_options = ['clear']
|
||||
|
||||
def func(self):
|
||||
if not self.args:
|
||||
self.list_styles()
|
||||
return
|
||||
self.set()
|
||||
|
||||
def list_styles(self):
|
||||
styles_table = self.style_table('Option', 'Description', 'Type', 'Value', width=78)
|
||||
for op_key in self.account.options.options_dict.keys():
|
||||
op_found = self.account.options.get(op_key, return_obj=True)
|
||||
styles_table.add_row(op_key, op_found.description, op_found.__class__.__name__, op_found.display())
|
||||
self.msg(str(styles_table))
|
||||
|
||||
def set(self):
|
||||
try:
|
||||
result = self.account.options.set(self.lhs, self.rhs)
|
||||
except ValueError as e:
|
||||
self.msg(str(e))
|
||||
return
|
||||
self.msg('Success! The new value is: %s' % result)
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ def list_bans(banlist):
|
|||
if not banlist:
|
||||
return "No active bans were found."
|
||||
|
||||
table = evtable.EvTable("|wid", "|wname/ip", "|wdate", "|wreason")
|
||||
table = self.style_table("|wid", "|wname/ip", "|wdate", "|wreason")
|
||||
for inum, ban in enumerate(banlist):
|
||||
table.add_row(str(inum + 1),
|
||||
ban[0] and ban[0] or ban[1],
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class AccountCmdSet(CmdSet):
|
|||
self.add(account.CmdPassword())
|
||||
self.add(account.CmdColorTest())
|
||||
self.add(account.CmdQuell())
|
||||
self.add(account.CmdStyle())
|
||||
|
||||
# nicks
|
||||
self.add(general.CmdNick())
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
if self.cmdstring == "comlist":
|
||||
# just display the subscribed channels with no extra info
|
||||
comtable = evtable.EvTable("|wchannel|n", "|wmy aliases|n",
|
||||
comtable = self.style_table("|wchannel|n", "|wmy aliases|n",
|
||||
"|wdescription|n", align="l", maxwidth=_DEFAULT_WIDTH)
|
||||
for chan in subs:
|
||||
clower = chan.key.lower()
|
||||
|
|
@ -306,7 +306,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
|
|||
" |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s" % comtable)
|
||||
else:
|
||||
# full listing (of channels caller is able to listen to)
|
||||
comtable = evtable.EvTable("|wsub|n", "|wchannel|n", "|wmy aliases|n",
|
||||
comtable = self.style_table("|wsub|n", "|wchannel|n", "|wmy aliases|n",
|
||||
"|wlocks|n", "|wdescription|n", maxwidth=_DEFAULT_WIDTH)
|
||||
for chan in channels:
|
||||
clower = chan.key.lower()
|
||||
|
|
@ -815,7 +815,7 @@ def _list_bots():
|
|||
ircbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
|
||||
if ircbots:
|
||||
from evennia.utils.evtable import EvTable
|
||||
table = EvTable("|w#dbref|n", "|wbotname|n", "|wev-channel|n",
|
||||
table = self.style_table("|w#dbref|n", "|wbotname|n", "|wev-channel|n",
|
||||
"|wirc-channel|n", "|wSSL|n", maxwidth=_DEFAULT_WIDTH)
|
||||
for ircbot in ircbots:
|
||||
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
|
||||
|
|
@ -1051,7 +1051,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
|||
rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
|
||||
if rssbots:
|
||||
from evennia.utils.evtable import EvTable
|
||||
table = EvTable("|wdbid|n", "|wupdate rate|n", "|wev-channel",
|
||||
table = self.style_table("|wdbid|n", "|wupdate rate|n", "|wev-channel",
|
||||
"|wRSS feed URL|n", border="cells", maxwidth=_DEFAULT_WIDTH)
|
||||
for rssbot in rssbots:
|
||||
table.add_row(rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
if not nicklist:
|
||||
string = "|wNo nicks defined.|n"
|
||||
else:
|
||||
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
|
||||
table = self.style_table("#", "Type", "Nick match", "Replacement")
|
||||
for inum, nickobj in enumerate(nicklist):
|
||||
_, _, nickvalue, replacement = nickobj.value
|
||||
table.add_row(str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement))
|
||||
|
|
@ -338,7 +338,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
table = evtable.EvTable(border="header")
|
||||
table = self.style_table(border="header")
|
||||
for item in items:
|
||||
table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
||||
string = "|wYou are carrying:\n%s" % table
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
nobjs = nobjs or 1 # fix zero-div error with empty database
|
||||
|
||||
# total object sum table
|
||||
totaltable = EvTable("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n", border="table", align="l")
|
||||
totaltable = self.style_table("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n", border="table", align="l")
|
||||
totaltable.align = 'l'
|
||||
totaltable.add_row("Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100))
|
||||
totaltable.add_row("Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100))
|
||||
|
|
@ -453,7 +453,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
|
||||
|
||||
# typeclass table
|
||||
typetable = EvTable("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", align="l")
|
||||
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", align="l")
|
||||
typetable.align = 'l'
|
||||
dbtotals = ObjectDB.objects.object_totals()
|
||||
for path, count in dbtotals.items():
|
||||
|
|
@ -461,7 +461,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# last N table
|
||||
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
|
||||
latesttable = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
|
||||
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
|
||||
latesttable.align = 'l'
|
||||
for obj in objs:
|
||||
latesttable.add_row(utils.datetime_format(obj.date_created),
|
||||
|
|
@ -557,12 +557,12 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# typeclass table
|
||||
dbtotals = AccountDB.objects.object_totals()
|
||||
typetable = EvTable("|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l")
|
||||
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l")
|
||||
for path, count in dbtotals.items():
|
||||
typetable.add_row(path, count, "%.2f" % ((float(count) / naccounts) * 100))
|
||||
# last N table
|
||||
plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim):]
|
||||
latesttable = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
|
||||
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
|
||||
for ply in plyrs:
|
||||
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path)
|
||||
|
||||
|
|
@ -613,7 +613,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
if not switches or switches[0] == "list":
|
||||
# Just display the list of installed services and their
|
||||
# status, then exit.
|
||||
table = EvTable("|wService|n (use @services/start|stop|delete)", "|wstatus", align="l")
|
||||
table = self.style_table("|wService|n (use @services/start|stop|delete)", "|wstatus", align="l")
|
||||
for service in service_collection.services:
|
||||
table.add_row(service.name, service.running and "|gRunning" or "|rNot Running")
|
||||
caller.msg(str(table))
|
||||
|
|
@ -723,14 +723,14 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
def func(self):
|
||||
"""Show server time data in a table."""
|
||||
table1 = EvTable("|wServer time", "", align="l", width=78)
|
||||
table1 = self.style_table("|wServer time", "", align="l", width=78)
|
||||
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
|
||||
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
|
||||
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
|
||||
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
|
||||
table1.add_row("Current time", datetime.datetime.now())
|
||||
table1.reformat_column(0, width=30)
|
||||
table2 = EvTable("|wIn-Game time", "|wReal time x %g" % gametime.TIMEFACTOR, align="l", width=77, border_top=0)
|
||||
table2 = self.style_table("|wIn-Game time", "|wReal time x %g" % gametime.TIMEFACTOR, align="l", width=77, border_top=0)
|
||||
epochtxt = "Epoch (%s)" % ("from settings" if settings.TIME_GAME_EPOCH else "server start")
|
||||
table2.add_row(epochtxt, datetime.datetime.fromtimestamp(gametime.game_epoch()))
|
||||
table2.add_row("Total time passed:", utils.time_format(gametime.gametime(), 2))
|
||||
|
|
@ -824,7 +824,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|||
self.caller.msg(string % (rmem, pmem))
|
||||
return
|
||||
# Display table
|
||||
loadtable = EvTable("property", "statistic", align="l")
|
||||
loadtable = self.style_table("property", "statistic", align="l")
|
||||
loadtable.add_row("Total CPU load", "%g %%" % loadavg)
|
||||
loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
|
||||
loadtable.add_row("Process ID", "%g" % pid),
|
||||
|
|
@ -850,7 +850,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|||
self.caller.msg(string % (rmem, pmem, vmem))
|
||||
return
|
||||
|
||||
loadtable = EvTable("property", "statistic", align="l")
|
||||
loadtable = self.style_table("property", "statistic", align="l")
|
||||
loadtable.add_row("Server load (1 min)", "%g" % loadavg)
|
||||
loadtable.add_row("Process ID", "%g" % pid),
|
||||
loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
|
||||
|
|
@ -875,7 +875,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|||
total_num, cachedict = _IDMAPPER.cache_size()
|
||||
sorted_cache = sorted([(key, num) for key, num in cachedict.items() if num > 0],
|
||||
key=lambda tup: tup[1], reverse=True)
|
||||
memtable = EvTable("entity name", "number", "idmapper %", align="l")
|
||||
memtable = self.style_table("entity name", "number", "idmapper %", align="l")
|
||||
for tup in sorted_cache:
|
||||
memtable.add_row(tup[0], "%i" % tup[1], "%.2f" % (float(tup[1]) / total_num * 100))
|
||||
|
||||
|
|
@ -907,7 +907,7 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
|
|||
if not all_subs:
|
||||
self.caller.msg("No tickers are currently active.")
|
||||
return
|
||||
table = EvTable("interval (s)", "object", "path/methodname", "idstring", "db")
|
||||
table = self.style_table("interval (s)", "object", "path/methodname", "idstring", "db")
|
||||
for sub in all_subs:
|
||||
table.add_row(sub[3],
|
||||
"%s%s" % (sub[0] or "[None]",
|
||||
|
|
|
|||
|
|
@ -489,6 +489,42 @@ START_LOCATION = "#2"
|
|||
# issues.
|
||||
TYPECLASS_AGGRESSIVE_CACHE = True
|
||||
|
||||
######################################################################
|
||||
# Options and validators
|
||||
######################################################################
|
||||
|
||||
# Replace or add entries in this dictionary to specify options available
|
||||
# on accounts. An option goes goteth
|
||||
|
||||
# Evennia uses for commands. Or add more entries! Accounts can change
|
||||
# their own settings with a command, but this sets down defaults.
|
||||
|
||||
# Option tuples are in this format:
|
||||
# ("Description", 'Option Class', 'Default Value')
|
||||
|
||||
OPTIONS_ACCOUNT_DEFAULT = {
|
||||
'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'),
|
||||
'header_star_color': ('* inside Header lines.', 'Color', 'm'),
|
||||
'header_text_color': ('Text inside Header lines.', 'Color', 'w'),
|
||||
'footer_text_color': ('Text inside Footer Lines.', 'Color', 'w'),
|
||||
'column_names_color': ('Table column header text.', 'Color', 'G'),
|
||||
'header_fill': ('Fill for Header lines.', 'Text', '='),
|
||||
'separator_fill': ('Fill for Separator Lines.', 'Text', '-'),
|
||||
'footer_fill': ('Fill for Footer Lines.', 'Text', '='),
|
||||
'help_category_color': ('Help category names.', 'Color', 'g'),
|
||||
'help_entry_color': ('Help entry names.', 'Color', 'c'),
|
||||
'timezone': ('Timezone for dates. @tz for a list.', 'Timezone', 'UTC')
|
||||
}
|
||||
# Modules holding Option classes, responsible for serializing the option and
|
||||
# calling validator functions on it. Same-named functions in modules added
|
||||
# later in this list will override those added earlier.
|
||||
OPTION_MODULES = ['evennia.utils.optionclasses', ]
|
||||
# Module holding validator functions. These are used as a resource for
|
||||
# validating options, but can also be used as input validators in general.#
|
||||
# Same-named functions in modules added later in this list will override those
|
||||
# added earlier.
|
||||
VALIDATOR_MODULES = ['evennia.utils.validatorfunctions', ]
|
||||
|
||||
######################################################################
|
||||
# Batch processors
|
||||
######################################################################
|
||||
|
|
@ -521,7 +557,7 @@ TIME_GAME_EPOCH = None
|
|||
TIME_IGNORE_DOWNTIMES = False
|
||||
|
||||
######################################################################
|
||||
# Inlinefunc & PrototypeFuncs
|
||||
# Inlinefunc, PrototypeFuncs
|
||||
######################################################################
|
||||
# Evennia supports inline function preprocessing. This allows users
|
||||
# to supply inline calls on the form $func(arg, arg, ...) to do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Containers
|
|||
|
||||
|
||||
from django.conf import settings
|
||||
from evennia.utils.utils import class_from_module
|
||||
from evennia.utils.utils import class_from_module, callables_from_module
|
||||
from evennia.utils import logger
|
||||
|
||||
|
||||
|
|
@ -108,3 +108,48 @@ class GlobalScriptContainer(object):
|
|||
|
||||
# Create singleton of the GlobalHandler for the API.
|
||||
GLOBAL_SCRIPTS = GlobalScriptContainer()
|
||||
|
||||
|
||||
class ValidatorContainer(object):
|
||||
"""
|
||||
Loads and stores the final list of VALIDATOR FUNCTIONS.
|
||||
|
||||
Can access these as properties or dictionary-contents.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid_storage = {}
|
||||
for module in settings.VALIDATOR_FUNC_MODULES:
|
||||
self.valid_storage.update(callables_from_module(module))
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.valid_storage.get(item, None)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
|
||||
|
||||
# Ensure that we have a Singleton of ValidHandler that is always loaded... and only needs to be loaded once.
|
||||
VALIDATOR_FUNCS = ValidatorContainer()
|
||||
|
||||
|
||||
class OptionContainer(object):
|
||||
"""
|
||||
Loads and stores the final list of OPTION CLASSES.
|
||||
|
||||
Can access these as properties or dictionary-contents.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.option_storage = {}
|
||||
for module in settings.OPTION_CLASS_MODULES:
|
||||
self.option_storage.update(callables_from_module(module))
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.option_storage.get(item, None)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
|
||||
|
||||
# Ensure that we have a Singleton that keeps all loaded Option classes
|
||||
OPTION_CLASSES = OptionContainer()
|
||||
|
|
|
|||
116
evennia/utils/option.py
Normal file
116
evennia/utils/option.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from evennia.utils.utils import string_partial_matching
|
||||
from evennia.utils.containers import OPTION_CONTAINER
|
||||
|
||||
|
||||
class OptionHandler(object):
|
||||
"""
|
||||
This is a generic Option handler meant for Typed Objects - anything that implements AttributeHandler.
|
||||
|
||||
It uses a dictionary to store-and-cache frequently used settings such as colors for borders or an
|
||||
account's timezone.
|
||||
|
||||
This is used for Account.options but it could be used by Scripts or Objects just as easily. All
|
||||
it needs to be provided is an options_dict.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, options_dict=None, save_category=None):
|
||||
"""
|
||||
Initialize an OptionHandler.
|
||||
|
||||
Args:
|
||||
obj (TypedObject): The Typed Object this sits on. Obj MUST implement the Evennia AttributeHandler
|
||||
or this will barf.
|
||||
options_dict (dict): A dictionary of option keys, where the values are options. The format of those
|
||||
tuples is: ('key', "Description to show", 'option_type', <default value>)
|
||||
save_category (str): The Options data will be stored to this Attribute category on obj.
|
||||
"""
|
||||
if not options_dict:
|
||||
options_dict = dict()
|
||||
self.options_dict = options_dict
|
||||
self.save_category = save_category
|
||||
self.obj = obj
|
||||
|
||||
# This dictionary stores the in-memory Options by their key. Values are the Option objects.
|
||||
self.options = dict()
|
||||
|
||||
# We use lazy-loading of each Option when it's called for, but it's good to have the save data
|
||||
# on hand.
|
||||
self.save_data = {s.key: s.value for s in obj.attributes.get(category=save_category,
|
||||
return_list=True, return_obj=True) if s}
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
Shortcut to self.get(item) used as a different syntax. This entire object is
|
||||
essentially a dictionary of option_key -> value.
|
||||
|
||||
Args:
|
||||
item (str): The Key of the item to get.
|
||||
|
||||
Returns:
|
||||
The Option's value.
|
||||
"""
|
||||
return self.get(item).value
|
||||
|
||||
def get(self, item, return_obj=False):
|
||||
"""
|
||||
Retrieves an Option stored in the handler. Will load it if it doesn't exist.
|
||||
|
||||
Args:
|
||||
item (str): The key to retrieve.
|
||||
return_obj (bool): If True, returns the actual option object instead of its value.
|
||||
|
||||
Returns:
|
||||
An option value (varies) or the Option itself.
|
||||
"""
|
||||
if item not in self.options_dict:
|
||||
raise KeyError("Option not found!")
|
||||
if item in self.options:
|
||||
op_found = self.options[item]
|
||||
else:
|
||||
op_found = self._load_option(item)
|
||||
if return_obj:
|
||||
return op_found
|
||||
return op_found.value
|
||||
|
||||
def _load_option(self, key):
|
||||
"""
|
||||
Loads option on-demand if it has not been loaded yet.
|
||||
|
||||
Args:
|
||||
key (str): The option being loaded.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
option_def = self.options_dict[key]
|
||||
save_data = self.save_data.get(key, None)
|
||||
self.obj.msg(save_data)
|
||||
loaded_option = OPTION_CONTAINER[option_def[1]](self, key, option_def[0], option_def[2], save_data)
|
||||
self.options[key] = loaded_option
|
||||
return loaded_option
|
||||
|
||||
def set(self, option, value, **kwargs):
|
||||
"""
|
||||
Change an individual option.
|
||||
|
||||
Args:
|
||||
option (str): The key of an option that can be changed. Allows partial matching.
|
||||
value (str): The value that should be checked, coerced, and stored.
|
||||
|
||||
Returns:
|
||||
New value
|
||||
"""
|
||||
if not option:
|
||||
raise ValueError("Option field blank!")
|
||||
found = string_partial_matching(list(self.options_dict.keys()), option, ret_index=False)
|
||||
if not found:
|
||||
raise ValueError("Option not found!")
|
||||
if len(found) > 1:
|
||||
raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.")
|
||||
found = found[0]
|
||||
op = self.get(found, return_obj=True)
|
||||
op.set(value, **kwargs)
|
||||
return op.display()
|
||||
|
||||
|
||||
|
||||
279
evennia/utils/optionclasses.py
Normal file
279
evennia/utils/optionclasses.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import datetime as _dt
|
||||
from evennia import logger as _log
|
||||
from evennia.utils.ansi import ANSIString as _ANSI
|
||||
from evennia.utils.validatorfunctions import _TZ_DICT
|
||||
from evennia.utils.containers import VALIDATOR_CONTAINER as _VAL
|
||||
|
||||
|
||||
class BaseOption(object):
|
||||
"""
|
||||
Abstract Class to deal with encapsulating individual Options. An Option has a name/key, a description
|
||||
to display in relevant commands and menus, and a default value. It saves to the owner's Attributes using
|
||||
its Handler's save category.
|
||||
|
||||
Designed to be extremely overloadable as some options can be cantankerous.
|
||||
|
||||
Properties:
|
||||
valid: Shortcut to the loaded VALID_HANDLER.
|
||||
validator_key (str): The key of the Validator this uses.
|
||||
"""
|
||||
validator_key = ''
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
def __init__(self, handler, key, description, default, save_data=None):
|
||||
"""
|
||||
|
||||
Args:
|
||||
handler (OptionHandler): The OptionHandler that 'owns' this Option.
|
||||
key (str): The name this will be used for storage in a dictionary. Must be unique per
|
||||
OptionHandler.
|
||||
description (str): What this Option's text will show in commands and menus.
|
||||
default: A default value for this Option.
|
||||
save_data: Whatever was saved to Attributes. This differs by Option.
|
||||
"""
|
||||
self.handler = handler
|
||||
self.key = key
|
||||
self.default_value = default
|
||||
self.description = description
|
||||
self.save_data = save_data
|
||||
|
||||
# Value Storage contains None until the Option is loaded.
|
||||
self.value_storage = None
|
||||
|
||||
# And it's not loaded until it's called upon to spit out its contents.
|
||||
self.loaded = False
|
||||
|
||||
def display(self, **kwargs):
|
||||
"""
|
||||
Renders the Option's value as something pretty to look at.
|
||||
|
||||
Returns:
|
||||
How the stored value should be projected to users. a raw timedelta is pretty ugly, y'know?
|
||||
"""
|
||||
return self.value
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Takes the provided save data, validates it, and gets this Option ready to use.
|
||||
|
||||
Returns:
|
||||
Boolean: Whether loading was successful.
|
||||
"""
|
||||
if self.save_data is not None:
|
||||
try:
|
||||
self.value_storage = self.deserialize(self.save_data)
|
||||
self.loaded = True
|
||||
return True
|
||||
except Exception as e:
|
||||
_log.log_trace(e)
|
||||
return False
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Exports the current value to an Attribute.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.handler.obj.attributes.add(self.key, category=self.handler.save_category, value=self.serialize())
|
||||
|
||||
def deserialize(self, save_data):
|
||||
"""
|
||||
Perform sanity-checking on the save data. This isn't the same as Validators, as Validators deal with
|
||||
user input. save data might be a timedelta or a list or some other object. isinstance() is probably
|
||||
very useful here.
|
||||
|
||||
Args:
|
||||
save_data: The data to check.
|
||||
|
||||
Returns:
|
||||
Arbitrary: Whatever the Option needs to track, like a string or a datetime. Not the same as what
|
||||
users are SHOWN.
|
||||
"""
|
||||
return save_data
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Serializes the save data for Attribute storage if it's something complicated.
|
||||
|
||||
Returns:
|
||||
Whatever best handles the Attribute.
|
||||
"""
|
||||
return self.value_storage
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
return self.value_storage != self.default_value
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
return self.default_value
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.loaded and self.save_data is not None:
|
||||
self.load()
|
||||
if self.loaded:
|
||||
return self.value_storage
|
||||
else:
|
||||
return self.default
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self.set(value)
|
||||
|
||||
def set(self, value, **kwargs):
|
||||
"""
|
||||
Takes user input, presumed to be a string, and changes the value if it is a valid input.
|
||||
|
||||
Args:
|
||||
value: The new value of this Option.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
final_value = self.validate(value, **kwargs)
|
||||
self.value_storage = final_value
|
||||
self.loaded = True
|
||||
self.save()
|
||||
|
||||
def validate(self, value, **kwargs):
|
||||
"""
|
||||
Validate user input, which is presumed to be a string.
|
||||
|
||||
Args:
|
||||
value (str): User input.
|
||||
account (AccountDB): The Account that is performing the validation. This is necessary because of
|
||||
other settings which may affect the check, such as an Account's timezone affecting how their
|
||||
datetime entries are processed.
|
||||
|
||||
Returns:
|
||||
The results of a Validator call. Might be any kind of python object.
|
||||
"""
|
||||
return _VAL[self.validator_key](value, thing_name=self.key, **kwargs)
|
||||
|
||||
|
||||
class Text(BaseOption):
|
||||
validator_key = 'text'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
got_data = str(save_data)
|
||||
if not got_data:
|
||||
raise ValueError(f"{self.key} expected Text data, got '{save_data}'")
|
||||
return got_data
|
||||
|
||||
|
||||
class Email(BaseOption):
|
||||
validator_key = 'email'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
got_data = str(save_data)
|
||||
if not got_data:
|
||||
raise ValueError(f"{self.key} expected String data, got '{save_data}'")
|
||||
return got_data
|
||||
|
||||
|
||||
class Boolean(BaseOption):
|
||||
validator_key = 'boolean'
|
||||
|
||||
def display(self, **kwargs):
|
||||
if self.value:
|
||||
return '1 - On/True'
|
||||
return '0 - Off/False'
|
||||
|
||||
def serialize(self):
|
||||
return self.value
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if not isinstance(save_data, bool):
|
||||
raise ValueError(f"{self.key} expected Boolean, got '{save_data}'")
|
||||
return save_data
|
||||
|
||||
|
||||
class Color(BaseOption):
|
||||
validator_key = 'color'
|
||||
|
||||
def display(self, **kwargs):
|
||||
return f'{self.value} - |{self.value}this|n'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if not save_data or len(_ANSI(f'|{save_data}|n')) > 0:
|
||||
raise ValueError(f"{self.key} expected Color Code, got '{save_data}'")
|
||||
return save_data
|
||||
|
||||
|
||||
class Timezone(BaseOption):
|
||||
validator_key = 'timezone'
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
return _TZ_DICT[self.default_value]
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if save_data not in _TZ_DICT:
|
||||
raise ValueError(f"{self.key} expected Timezone Data, got '{save_data}'")
|
||||
return _TZ_DICT[save_data]
|
||||
|
||||
def serialize(self):
|
||||
return str(self.value_storage)
|
||||
|
||||
|
||||
class UnsignedInteger(BaseOption):
|
||||
validator_key = 'unsigned_integer'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if isinstance(save_data, int) and save_data >= 0:
|
||||
return save_data
|
||||
raise ValueError(f"{self.key} expected Whole Number 0+, got '{save_data}'")
|
||||
|
||||
|
||||
class SignedInteger(BaseOption):
|
||||
validator_key = 'signed_integer'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if isinstance(save_data, int):
|
||||
return save_data
|
||||
raise ValueError(f"{self.key} expected Whole Number, got '{save_data}'")
|
||||
|
||||
|
||||
class PositiveInteger(BaseOption):
|
||||
validator_key = 'positive_integer'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if isinstance(save_data, int) and save_data > 0:
|
||||
return save_data
|
||||
raise ValueError(f"{self.key} expected Whole Number 1+, got '{save_data}'")
|
||||
|
||||
|
||||
class Duration(BaseOption):
|
||||
validator_key = 'duration'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if isinstance(save_data, int):
|
||||
return _dt.timedelta(0, save_data, 0, 0, 0, 0, 0)
|
||||
raise ValueError(f"{self.key} expected Timedelta in seconds, got '{save_data}'")
|
||||
|
||||
def serialize(self):
|
||||
return self.value_storage.seconds
|
||||
|
||||
|
||||
class Datetime(BaseOption):
|
||||
validator_key = 'datetime'
|
||||
|
||||
def deserialize(self, save_data):
|
||||
if isinstance(save_data, int):
|
||||
return _dt.datetime.utcfromtimestamp(save_data)
|
||||
raise ValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'")
|
||||
|
||||
def serialize(self):
|
||||
return int(self.value_storage.strftime('%s'))
|
||||
|
||||
|
||||
class Future(Datetime):
|
||||
validator_key = 'future'
|
||||
|
||||
|
||||
class Lock(Text):
|
||||
validator_key = 'lock'
|
||||
204
evennia/utils/validatorfunctions.py
Normal file
204
evennia/utils/validatorfunctions.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
Contains all the validation functions.
|
||||
|
||||
All validation functions must have a checker (probably a session) and entry arg.
|
||||
|
||||
They can employ more paramters at your leisure.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import re as _re
|
||||
import pytz as _pytz
|
||||
import datetime as _dt
|
||||
from django.core.exceptions import ValidationError as _error
|
||||
from django.core.validators import validate_email as _val_email
|
||||
from evennia.utils.ansi import ANSIString as _ansi
|
||||
from evennia.utils.utils import string_partial_matching as _partial
|
||||
|
||||
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
|
||||
|
||||
|
||||
def color(entry, thing_name='Color', **kwargs):
|
||||
if not entry:
|
||||
raise ValueError(f"Nothing entered for a {thing_name}!")
|
||||
test_str = _ansi('|%s|n' % entry)
|
||||
if len(test_str):
|
||||
raise ValueError(f"'{entry}' is not a valid {thing_name}.")
|
||||
return entry
|
||||
|
||||
|
||||
def datetime(entry, thing_name='Datetime', account=None, from_tz=None, **kwargs):
|
||||
"""
|
||||
Process a datetime string in standard forms while accounting for the inputter's timezone.
|
||||
|
||||
Args:
|
||||
entry (str): A date string from a user.
|
||||
thing_name (str): Name to display this datetime as.
|
||||
account (AccountDB): The Account performing this lookup. Unless from_tz is provided,
|
||||
account's timezone will be used (if found) for local time and convert the results
|
||||
to UTC.
|
||||
from_tz (pytz): An instance of pytz from the user. If not provided, defaults to whatever
|
||||
the Account uses. If neither one is provided, defaults to UTC.
|
||||
|
||||
Returns:
|
||||
datetime in utc.
|
||||
"""
|
||||
if not entry:
|
||||
raise ValueError(f"No {thing_name} entered!")
|
||||
if not from_tz:
|
||||
from_tz = _pytz.UTC
|
||||
utc = _pytz.UTC
|
||||
now = _dt.datetime.utcnow().replace(tzinfo=utc)
|
||||
cur_year = now.strftime('%Y')
|
||||
split_time = entry.split(' ')
|
||||
if len(split_time) == 3:
|
||||
entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {cur_year}"
|
||||
elif len(split_time) == 4:
|
||||
entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {split_time[3]}"
|
||||
else:
|
||||
raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}")
|
||||
try:
|
||||
local = _dt.datetime.strptime(input, '%b %d %H:%M %Y')
|
||||
except ValueError:
|
||||
raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}")
|
||||
local_tz = from_tz.localize(local)
|
||||
return local_tz.astimezone(utc)
|
||||
|
||||
|
||||
def duration(entry, thing_name='Duration', **kwargs):
|
||||
"""
|
||||
Take a string and derive a datetime timedelta from it.
|
||||
|
||||
Args:
|
||||
entry (string): This is a string from user-input. The intended format is, for example: "5d 2w 90s" for
|
||||
'five days, two weeks, and ninety seconds.' Invalid sections are ignored.
|
||||
thing_name (str): Name to display this query as.
|
||||
|
||||
Returns:
|
||||
timedelta
|
||||
|
||||
"""
|
||||
time_string = entry.split(" ")
|
||||
seconds = 0
|
||||
minutes = 0
|
||||
hours = 0
|
||||
days = 0
|
||||
weeks = 0
|
||||
|
||||
for interval in time_string:
|
||||
if _re.match(r'^[\d]+s$', interval.lower()):
|
||||
seconds =+ int(interval.lower().rstrip("s"))
|
||||
elif _re.match(r'^[\d]+m$', interval):
|
||||
minutes =+ int(interval.lower().rstrip("m"))
|
||||
elif _re.match(r'^[\d]+h$', interval):
|
||||
hours =+ int(interval.lower().rstrip("h"))
|
||||
elif _re.match(r'^[\d]+d$', interval):
|
||||
days =+ int(interval.lower().rstrip("d"))
|
||||
elif _re.match(r'^[\d]+w$', interval):
|
||||
weeks =+ int(interval.lower().rstrip("w"))
|
||||
elif _re.match(r'^[\d]+y$', interval):
|
||||
days =+ int(interval.lower().rstrip("y")) * 365
|
||||
else:
|
||||
raise ValueError(f"Could not convert section '{interval}' to a {thing_name}.")
|
||||
|
||||
return _dt.timedelta(days, seconds, 0, 0, minutes, hours, weeks)
|
||||
|
||||
|
||||
def future(entry, thing_name="Future Datetime", from_tz=None, **kwargs):
|
||||
time = datetime(entry, thing_name)
|
||||
if time < _dt.datetime.utcnow():
|
||||
raise ValueError(f"That {thing_name} is in the past! Must give a Future datetime!")
|
||||
return time
|
||||
|
||||
|
||||
def signed_integer(entry, thing_name="Signed Integer", **kwargs):
|
||||
if not entry:
|
||||
raise ValueError(f"Must enter a whole number for {thing_name}!")
|
||||
try:
|
||||
num = int(entry)
|
||||
except ValueError:
|
||||
raise ValueError(f"Could not convert '{entry}' to a whole number for {thing_name}!")
|
||||
return num
|
||||
|
||||
|
||||
def positive_integer(entry, thing_name="Positive Integer", **kwargs):
|
||||
num = signed_integer(entry, thing_name)
|
||||
if not num >= 1:
|
||||
raise ValueError(f"Must enter a whole number greater than 0 for {thing_name}!")
|
||||
return num
|
||||
|
||||
|
||||
def unsigned_integer(entry, thing_name="Unsigned Integer", **kwargs):
|
||||
num = signed_integer(entry, thing_name)
|
||||
if not num >= 0:
|
||||
raise ValueError(f"{thing_name} must be a whole number greater than or equal to 0!")
|
||||
return num
|
||||
|
||||
|
||||
def boolean(entry, thing_name="True/False", **kwargs):
|
||||
"""
|
||||
Simplest check in computer logic, right? This will take user input to flick the switch on or off
|
||||
Args:
|
||||
entry (str): A value such as True, On, Enabled, Disabled, False, 0, or 1.
|
||||
thing_name (str): What kind of Boolean we are setting. What Option is this for?
|
||||
|
||||
Returns:
|
||||
Boolean
|
||||
"""
|
||||
entry = entry.upper()
|
||||
error = f"Must enter 0 (false) or 1 (true) for {thing_name}. Also accepts True, False, On, Off, Yes, No, Enabled, and Disabled"
|
||||
if not entry:
|
||||
raise ValueError(error)
|
||||
if entry in ('1', 'TRUE', 'ON', 'ENABLED', 'ENABLE', 'YES'):
|
||||
return True
|
||||
if entry in ('0', 'FALSE', 'OFF', 'DISABLED', 'DISABLE', 'NO'):
|
||||
return False
|
||||
raise ValueError(error)
|
||||
|
||||
|
||||
def timezone(entry, thing_name="Timezone", **kwargs):
|
||||
"""
|
||||
Takes user input as string, and partial matches a Timezone.
|
||||
|
||||
Args:
|
||||
entry (str): The name of the Timezone.
|
||||
thing_name (str): What this Timezone is used for.
|
||||
|
||||
Returns:
|
||||
A PYTZ timezone.
|
||||
"""
|
||||
if not entry:
|
||||
raise ValueError(f"No {thing_name} entered!")
|
||||
found = _partial(list(_TZ_DICT.keys()), entry, ret_index=False)
|
||||
if len(found) > 1:
|
||||
raise ValueError(f"That matched: {', '.join(str(t) for t in found)}. Please be more specific!")
|
||||
if found:
|
||||
return _TZ_DICT[found[0]]
|
||||
raise ValueError(f"Could not find timezone '{entry}' for {thing_name}!")
|
||||
|
||||
|
||||
def email(entry, thing_name="Email Address", **kwargs):
|
||||
if not entry:
|
||||
raise ValueError("Email address field empty!")
|
||||
try:
|
||||
_val_email(entry) # offloading the hard work to Django!
|
||||
except _error:
|
||||
raise ValueError(f"That isn't a valid {thing_name}!")
|
||||
return entry
|
||||
|
||||
|
||||
def lock(entry, thing_name='locks', access_options=None, **kwargs):
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
raise ValueError(f"No {thing_name} entered to set!")
|
||||
for locksetting in entry.split(';'):
|
||||
access_type, lockfunc = locksetting.split(':', 1)
|
||||
if not access_type:
|
||||
raise ValueError("Must enter an access type!")
|
||||
if access_options:
|
||||
if access_type not in access_options:
|
||||
raise ValueError(f"Access type must be one of: {', '.join(access_options)}")
|
||||
if not lockfunc:
|
||||
raise ValueError("Lock func not entered.")
|
||||
return entry
|
||||
Loading…
Add table
Add a link
Reference in a new issue