Merge changes from master

This commit is contained in:
Griatch 2020-07-15 18:07:45 +02:00
commit 68e047dfd2
24 changed files with 324 additions and 244 deletions

View file

@ -73,6 +73,9 @@ without arguments starts a full interactive Python console.
required by Django.
- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch
to `spawn` command to extract the raw prototype dict for manual editing.
- `list_to_string` is now `iter_to_string` (but old name still works as legacy alias). It will
now accept any input, including generators and single values.
## Evennia 0.9 (2018-2019)

View file

@ -31,7 +31,7 @@ important if referring to newer Evennia documentation.
If you are new to Evennia it's *highly* recommended that you run through the
instructions in full - including initializing and starting a new empty game and connecting to it.
That way you can be sure Evennia works correctly as a base line. If you have trouble, make sure to
read the [Troubleshooting instructions](Getting-Started#troubleshooting) for your
read the [Troubleshooting instructions](./Getting-Started#troubleshooting) for your
operating system. You can also drop into our
[forums](https://groups.google.com/forum/#%21forum/evennia), join `#evennia` on `irc.freenode.net`
or chat from the linked [Discord Server](https://discord.gg/NecFePw).

View file

@ -68,10 +68,10 @@ Twisted packages
## Linux Install
If you run into any issues during the installation and first start, please
check out [Linux Troubleshooting](Getting-Started#linux-troubleshooting).
check out [Linux Troubleshooting](./Getting-Started#linux-troubleshooting).
For Debian-derived systems (like Ubuntu, Mint etc), start a terminal and
install the [dependencies](Getting-Started#requirements):
install the [dependencies](./Getting-Started#requirements):
```
sudo apt-get update
@ -175,7 +175,7 @@ evennia start # (create a superuser when asked. Email is optional.)
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](Getting-Started#where-to-go-next).
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Mac Install
@ -184,7 +184,7 @@ The Evennia server is a terminal program. Open the terminal e.g. from
*Applications->Utilities->Terminal*. [Here is an introduction to the Mac
terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line)
if you are unsure how it works. If you run into any issues during the
installation, please check out [Mac Troubleshooting](Getting-Started#mac-troubleshooting).
installation, please check out [Mac Troubleshooting](./Getting-Started#mac-troubleshooting).
* Python should already be installed but you must make sure it's a high enough version.
([This](http://docs.python-guide.org/en/latest/starting/install/osx/) discusses
@ -287,13 +287,13 @@ evennia start # (create a superuser when asked. Email is optional.)
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](Getting-Started#where-to-go-next).
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Windows Install
If you run into any issues during the installation, please check out
[Windows Troubleshooting](Getting-Started#windows-troubleshooting).
[Windows Troubleshooting](./Getting-Started#windows-troubleshooting).
> If you are running Windows10, consider using the Windows Subsystem for Linux
> ([WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)) instead.
@ -428,7 +428,7 @@ evennia start # (create a superuser when asked. Email is optional.)
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](Getting-Started#where-to-go-next).
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Where to Go Next

View file

@ -120,7 +120,7 @@ $ cd ~ && source evenv/bin/activate
(evenv) $ evennia start
```
You may wish to look at the [Linux Instructions](Getting-Started#linux-install) for more.
You may wish to look at the [Linux Instructions](./Getting-Started#linux-install) for more.
## Caveats

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -651,6 +651,51 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
logger.log_sec(f"Password successfully changed for {self}.")
self.at_password_change()
def create_character(self, *args, **kwargs):
"""
Create a character linked to this account.
Args:
key (str, optional): If not given, use the same name as the account.
typeclass (str, optional): Typeclass to use for this character. If
not given, use settings.BASE_CHARACTER_TYPECLASS.
permissions (list, optional): If not given, use the account's permissions.
ip (str, optiona): The client IP creating this character. Will fall back to the
one stored for the account if not given.
kwargs (any): Other kwargs will be used in the create_call.
Returns:
Object: A new character of the `character_typeclass` type. None on an error.
list or None: A list of errors, or None.
"""
# parse inputs
character_key = kwargs.pop("key", self.key)
character_ip = kwargs.pop("ip", self.db.creator_ip)
character_permissions = kwargs.pop("permissions", self.permissions)
# Load the appropriate Character class
character_typeclass = kwargs.pop("typeclass", None)
character_typeclass = character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS
Character = class_from_module(character_typeclass)
# Create the character
character, errs = Character.create(
character_key,
self,
ip=character_ip,
typeclass=character_typeclass,
permissions=character_permissions,
**kwargs
)
if character:
# Update playable character list
if character not in self.characters:
self.db._playable_characters.append(character)
# We need to set this to have @ic auto-connect to this character
self.db._last_puppet = character
return character, errs
@classmethod
def create(cls, *args, **kwargs):
"""
@ -759,31 +804,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
logger.log_err(string)
if account and settings.MULTISESSION_MODE < 2:
# Load the appropriate Character class
character_typeclass = kwargs.get(
"character_typeclass", settings.BASE_CHARACTER_TYPECLASS
)
character_home = kwargs.get("home")
Character = class_from_module(character_typeclass)
# Auto-create a character to go with this account
# Create the character
character, errs = Character.create(
account.key,
account,
ip=ip,
typeclass=character_typeclass,
permissions=permissions,
home=character_home,
)
errors.extend(errs)
if character:
# Update playable character list
if character not in account.characters:
account.db._playable_characters.append(character)
# We need to set this to have @ic auto-connect to this character
account.db._last_puppet = character
character, errs = account.create_character(typeclass=kwargs.get("character_typeclass"))
if errs:
errors.extend(errs)
except Exception:
# We are in the middle between logged in and -not, so we have
@ -1548,7 +1573,7 @@ class DefaultGuest(DefaultAccount):
try:
# Find an available guest name.
for name in settings.GUEST_LIST:
if not AccountDB.objects.filter(username__iexact=name).count():
if not AccountDB.objects.filter(username__iexact=name).exists():
username = name
break
if not username:
@ -1574,6 +1599,15 @@ class DefaultGuest(DefaultAccount):
ip=ip,
)
errors.extend(errs)
if not account.characters:
# this can happen for multisession_mode > 1. For guests we
# always auto-create a character, regardless of multi-session-mode.
character, errs = account.create_character()
if errs:
errors.extend(errs)
return account, errors
except Exception as e:

View file

@ -4,13 +4,26 @@
#
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.admin.utils import unquote
from django.template.response import TemplateResponse
from django.http import Http404, HttpResponseRedirect
from django.core.exceptions import PermissionDenied
from django.views.decorators.debug import sensitive_post_parameters
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.urls import path, reverse
from django.contrib.auth import update_session_auth_hash
from evennia.accounts.models import AccountDB
from evennia.typeclasses.admin import AttributeInline, TagInline
from evennia.utils import create
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
# handle the custom User editor
class AccountDBChangeForm(UserChangeForm):
@ -88,6 +101,7 @@ class AccountForm(forms.ModelForm):
class Meta(object):
model = AccountDB
fields = "__all__"
app_label = "accounts"
db_key = forms.RegexField(
label="Username",
@ -259,6 +273,71 @@ class AccountDBAdmin(BaseUserAdmin):
),
)
@sensitive_post_parameters_m
def user_change_password(self, request, id, form_url=''):
user = self.get_object(request, unquote(id))
if not self.has_change_permission(request, user):
raise PermissionDenied
if user is None:
raise Http404('%(name)s object with primary key %(key)r does not exist.') % {
'name': self.model._meta.verbose_name,
'key': escape(id),
}
if request.method == 'POST':
form = self.change_password_form(user, request.POST)
if form.is_valid():
form.save()
change_message = self.construct_change_message(request, form, None)
self.log_change(request, user, change_message)
msg = 'Password changed successfully.'
messages.success(request, msg)
update_session_auth_hash(request, form.user)
return HttpResponseRedirect(
reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
user._meta.app_label,
# the model_name is something we need to hardcode
# since our accountdb is a proxy:
"accountdb",
),
args=(user.pk,),
)
)
else:
form = self.change_password_form(user)
fieldsets = [(None, {'fields': list(form.base_fields)})]
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
context = {
'title': 'Change password: %s' % escape(user.get_username()),
'adminForm': adminForm,
'form_url': form_url,
'form': form,
'is_popup': (IS_POPUP_VAR in request.POST or
IS_POPUP_VAR in request.GET),
'add': True,
'change': False,
'has_delete_permission': False,
'has_change_permission': True,
'has_absolute_url': False,
'opts': self.model._meta,
'original': user,
'save_as': False,
'show_save': True,
**self.admin_site.each_context(request),
}
request.current_app = self.admin_site.name
return TemplateResponse(
request,
self.change_user_password_template or
'admin/auth/user/change_password.html',
context,
)
def save_model(self, request, obj, form, change):
"""
Custom save actions.

View file

@ -109,8 +109,8 @@ class AccountDB(TypedObject, AbstractUser):
__applabel__ = "accounts"
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
class Meta(object):
verbose_name = "Account"
# class Meta:
# verbose_name = "Account"
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch

View file

@ -472,13 +472,13 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
tempmergers[prio] = cmdset
# sort cmdsets after reverse priority (highest prio are merged in last)
cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)
sorted_cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)
# Merge all command sets into one, beginning with the lowest-prio one
cmdset = cmdsets[0]
for merging_cmdset in cmdsets[1:]:
cmdset = sorted_cmdsets[0]
for merging_cmdset in sorted_cmdsets[1:]:
cmdset = yield cmdset + merging_cmdset
# store the full sets for diagnosis
# store the original, ungrouped set for diagnosis
cmdset.merged_from = cmdsets
# cache
_CMDSET_MERGE_CACHE[mergehash] = cmdset

View file

@ -443,12 +443,12 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority, cmdset_a.key, cmdset_a.priority)
# return the system commands to the cmdset
cmdset_c.add(sys_commands)
cmdset_c.add(sys_commands, allow_duplicates=True)
return cmdset_c
def add(self, cmd):
def add(self, cmd, allow_duplicates=False):
"""
Add a new command or commands to this CmdSetcommand, a list of
Add a new command or commands to this CmdSet, a list of
commands or a cmdset to this cmdset. Note that this is *not*
a merge operation (that is handled by the + operator).
@ -456,6 +456,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
cmd (Command, list, Cmdset): This allows for adding one or
more commands to this Cmdset in one go. If another Cmdset
is given, all its commands will be added.
allow_duplicates (bool, optional): If set, will not try to remove
duplicate cmds in the set. This is needed during the merge process
to avoid wiping commands coming from cmdsets with duplicate=True.
Notes:
If cmd already exists in set, it will replace the old one
@ -498,8 +501,10 @@ class CmdSet(object, metaclass=_CmdSetMeta):
commands[ic] = cmd # replace
except ValueError:
commands.append(cmd)
# extra run to make sure to avoid doublets
self.commands = list(set(commands))
self.commands = commands
if not allow_duplicates:
# extra run to make sure to avoid doublets
self.commands = list(set(self.commands))
# add system_command to separate list as well,
# for quick look-up
if cmd.key.startswith("__"):

View file

@ -50,35 +50,6 @@ _UTF8_ERROR = """
Error reported was: '%s'
"""
_PROCPOOL_BATCHCMD_SOURCE = """
from evennia.commands.default.batchprocess import batch_cmd_exec, step_pointer, BatchSafeCmdSet
caller.ndb.batch_stack = commands
caller.ndb.batch_stackptr = 0
caller.ndb.batch_batchmode = "batch_commands"
caller.cmdset.add(BatchSafeCmdSet)
for inum in range(len(commands)):
print "command:", inum
caller.cmdset.add(BatchSafeCmdSet)
if not batch_cmd_exec(caller):
break
step_pointer(caller, 1)
print "leaving run ..."
"""
_PROCPOOL_BATCHCODE_SOURCE = """
from evennia.commands.default.batchprocess import batch_code_exec, step_pointer, BatchSafeCmdSet
caller.ndb.batch_stack = codes
caller.ndb.batch_stackptr = 0
caller.ndb.batch_batchmode = "batch_code"
caller.cmdset.add(BatchSafeCmdSet)
for inum in range(len(codes)):
print "code:", inum
caller.cmdset.add(BatchSafeCmdSet)
if not batch_code_exec(caller):
break
step_pointer(caller, 1)
print "leaving run ..."
"""
# -------------------------------------------------------------
# Helper functions
@ -300,42 +271,17 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
"for %s (this might take some time) ..." % python_path
)
procpool = False
if "PythonProcPool" in utils.server_services():
if utils.uses_database("sqlite3"):
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
else:
procpool = True
if procpool:
# run in parallel process
def callback(r):
caller.msg(" |GBatchfile '%s' applied." % python_path)
purge_processor(caller)
def errback(e):
caller.msg(" |RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(
_PROCPOOL_BATCHCMD_SOURCE,
commands=commands,
caller=caller,
at_return=callback,
at_err=errback,
)
else:
# run in-process (might block)
for _ in range(len(commands)):
# loop through the batch file
if not batch_cmd_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("|G%s" % string)
purge_processor(caller)
# run in-process (might block)
for _ in range(len(commands)):
# loop through the batch file
if not batch_cmd_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("|G%s" % string)
purge_processor(caller)
class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
@ -420,41 +366,16 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
else:
caller.msg("Running Batch-code processor - Automatic mode for %s ..." % python_path)
procpool = False
if "PythonProcPool" in utils.server_services():
if utils.uses_database("sqlite3"):
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
else:
procpool = True
if procpool:
# run in parallel process
def callback(r):
caller.msg(" |GBatchfile '%s' applied." % python_path)
purge_processor(caller)
def errback(e):
caller.msg(" |RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(
_PROCPOOL_BATCHCODE_SOURCE,
codes=codes,
caller=caller,
at_return=callback,
at_err=errback,
)
else:
# un in-process (will block)
for _ in range(len(codes)):
# loop through the batch file
if not batch_code_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("|G%s" % string)
purge_processor(caller)
for _ in range(len(codes)):
# loop through the batch file
if not batch_code_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("|G%s" % string)
purge_processor(caller)
# -------------------------------------------------------------

View file

@ -1407,6 +1407,7 @@ class CmdOpen(ObjManipCommand):
locks = "cmd:perm(open) or perm(Builder)"
help_category = "Building"
new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"
# a custom member method to chug out exits and do checks
def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
"""
@ -1452,10 +1453,11 @@ class CmdOpen(ObjManipCommand):
else:
# exit does not exist before. Create a new one.
lockstring = self.new_obj_lockstring.format(id=caller.id)
if not typeclass:
typeclass = settings.BASE_EXIT_TYPECLASS
exit_obj = create.create_object(
typeclass, key=exit_name, location=location, aliases=exit_aliases, report_to=caller
typeclass, key=exit_name, location=location, aliases=exit_aliases, locks=lockstring, report_to=caller
)
if exit_obj:
# storing a destination is what makes it an exit!

View file

@ -17,7 +17,7 @@ import datetime
from anything import Anything
from django.conf import settings
from mock import Mock, mock
from unittest.mock import patch, Mock, MagicMock
from evennia import DefaultRoom, DefaultExit, ObjectDB
from evennia.commands.default.cmdset_character import CharacterCmdSet
@ -56,6 +56,7 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# ------------------------------------------------------------
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class CommandTest(EvenniaTest):
"""
Tests a command
@ -518,7 +519,7 @@ class TestBuilding(CommandTest):
self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2")
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.")
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
with patch("evennia.commands.default.building.EvEditor") as mock_ed:
self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
mock_ed.assert_called_with(self.char1, Anything, Anything, key="Obj2/test3")
@ -802,7 +803,7 @@ class TestBuilding(CommandTest):
)
self.call(building.CmdDesc(), "", "Usage: ")
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
with patch("evennia.commands.default.building.EvEditor") as mock_ed:
self.call(building.CmdDesc(), "/edit")
mock_ed.assert_called_with(
self.char1,
@ -1017,9 +1018,9 @@ class TestBuilding(CommandTest):
}
)
]
with mock.patch(
with patch(
"evennia.commands.default.building.protlib.search_prototype",
new=mock.MagicMock(return_value=test_prototype),
new=MagicMock(return_value=test_prototype),
) as mprot:
self.call(
building.CmdTypeclass(),
@ -1085,7 +1086,7 @@ class TestBuilding(CommandTest):
self.call(building.CmdFind(), "/exact Obj", "One Match")
# Test multitype filtering
with mock.patch(
with patch(
"evennia.commands.default.building.CHAR_TYPECLASS",
"evennia.objects.objects.DefaultCharacter",
):
@ -1553,11 +1554,11 @@ class TestSystemCommands(CommandTest):
self.call(multimatch, "look", "")
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
@patch("evennia.commands.default.syscommands.ChannelDB")
def test_channelcommand(self, mock_channeldb):
channel = mock.MagicMock()
channel.msg = mock.MagicMock()
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
channel = MagicMock()
channel.msg = MagicMock()
mock_channeldb.objects.get_channel = MagicMock(return_value=channel)
self.call(syscommands.SystemSendToChannel(), "public:Hello")
channel.msg.assert_called()

View file

@ -539,7 +539,9 @@ def objtag(accessing_obj, accessed_obj, *args, **kwargs):
Only true if accessed_obj has the specified tag and optional
category.
"""
return bool(accessed_obj.tags.get(*args))
tagkey = args[0] if args else None
category = args[1] if len(args) > 1 else None
return bool(accessed_obj.tags.get(tagkey, category=category))
def inside(accessing_obj, accessed_obj, *args, **kwargs):

View file

@ -236,7 +236,7 @@ class LockHandler(object):
elist.append(_("Lock: lock-function '%s' is not available.") % funcstring)
continue
args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
kwargs = dict([arg.split("=", 1) for arg in rest.split(",") if arg and "=" in arg])
kwargs = dict([(part.strip() for part in arg.split("=", 1)) for arg in rest.split(",") if arg and "=" in arg])
lock_funcs.append((func, args, kwargs))
evalstring = evalstring.replace(funcstring, "%s")
if len(lock_funcs) < nfuncs:

View file

@ -25,7 +25,7 @@ from evennia.utils import logger
from evennia.utils.utils import make_iter, dbref, lazy_property
class ContentsHandler(object):
class ContentsHandler:
"""
Handles and caches the contents of an object to avoid excessive
lookups (this is done very often due to cmdhandler needing to look

View file

@ -73,7 +73,7 @@ class ScriptDBManager(TypedObjectManager):
Get all scripts in the database.
Args:
key (str, optional): Restrict result to only those
key (str or int, optional): Restrict result to only those
with matching key or dbref.
Returns:
@ -83,12 +83,9 @@ class ScriptDBManager(TypedObjectManager):
if key:
script = []
dbref = self.dbref(key)
if dbref or dbref == 0:
# return either [] or a valid list (never [None])
script = [res for res in [self.dbref_search(dbref)] if res]
if not script:
script = self.filter(db_key=key)
return script
if dbref:
return self.filter(id=dbref)
return self.filter(db_key__iexact=key.strip())
return self.all()
def delete_script(self, dbref):
@ -231,7 +228,7 @@ class ScriptDBManager(TypedObjectManager):
ostring = ostring.strip()
dbref = self.dbref(ostring)
if dbref or dbref == 0:
if dbref:
# this is a dbref, try to find the script directly
dbref_match = self.dbref_search(dbref)
if dbref_match and not (

View file

@ -13,6 +13,7 @@ import time
from os.path import dirname, abspath
from twisted.application import internet, service
from twisted.internet.task import LoopingCall
from twisted.internet import protocol, reactor
from twisted.python.log import ILogObserver
@ -20,6 +21,7 @@ import django
django.setup()
from django.conf import settings
from django.db import connection
import evennia
@ -101,10 +103,29 @@ except ImportError:
WEB_PLUGINS_MODULE = None
INFO_DICT["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
"copy 'evennia/game_template/server/conf/web_plugins.py to "
"mygame/server/conf."
)
_MAINTENANCE_COUNT = 0
def _portal_maintenance():
"""
Repeated maintenance tasks for the portal.
"""
global _MAINTENANCE_COUNT
_MAINTENANCE_COUNT += 1
if _MAINTENANCE_COUNT % (3600 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
# -------------------------------------------------------------
# Portal Service object
# -------------------------------------------------------------
@ -143,6 +164,9 @@ class Portal(object):
self.start_time = time.time()
self.maintenance_task = LoopingCall(_portal_maintenance)
self.maintenance_task.start(60, now=True) # call every minute
# in non-interactive portal mode, this gets overwritten by
# cmdline sent by the evennia launcher
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()

View file

@ -249,6 +249,8 @@ class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION_CLASS):
return
else:
return
# just to be sure
text = to_str(text)
flags = self.protocol_flags

View file

@ -6,7 +6,7 @@ Test AMP client
import pickle
from model_mommy import mommy
from unittest import TestCase
from mock import MagicMock, patch
from unittest.mock import MagicMock, patch
from twisted.trial.unittest import TestCase as TwistedTestCase
from evennia.server import amp_client
from evennia.server.portal import amp_server
@ -36,6 +36,7 @@ class _TestAMP(TwistedTestCase):
self.server.sessions[1] = self.session
self.portal = portal.Portal(MagicMock())
self.portal.maintenance_task.stop()
self.portalsession = session.Session()
self.portalsession.sessid = 1
self.portal.sessions[1] = self.portalsession

View file

@ -6,6 +6,7 @@ Runs as part of the Evennia's test suite with 'evennia test evennia"
"""
from django.test.runner import DiscoverRunner
from unittest import mock
class EvenniaTestSuiteRunner(DiscoverRunner):
@ -21,9 +22,16 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
Build a test suite for Evennia. test_labels is a list of apps to test.
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
# the portal looping call starts before the unit-test suite so we
# can't mock it - instead we stop it before starting the test - otherwise
# we'd get unclean reactor errors across test boundaries.
from evennia.server.portal.portal import PORTAL
PORTAL.maintenance_task.stop()
import evennia
evennia._init()
return super(EvenniaTestSuiteRunner, self).build_suite(
test_labels, extra_tests=extra_tests, **kwargs
)

View file

@ -110,10 +110,38 @@ class TypeclassBase(SharedMemoryModelBase):
attrs["typename"] = name
attrs["path"] = "%s.%s" % (attrs["__module__"], name)
# typeclass proxy setup
if "Meta" not in attrs:
def _get_dbmodel(bases):
"""Recursively get the dbmodel"""
if not hasattr(bases, "__iter__"):
bases = [bases]
for base in bases:
try:
if base._meta.proxy or base._meta.abstract:
for kls in base._meta.parents:
return _get_dbmodel(kls)
except AttributeError:
# this happens if trying to parse a non-typeclass mixin parent,
# without a _meta
continue
else:
return base
return None
class Meta(object):
dbmodel = _get_dbmodel(bases)
if not dbmodel:
raise TypeError(f"{name} does not appear to inherit from a database model.")
# typeclass proxy setup
# first check explicit __applabel__ on the typeclass, then figure
# it out from the dbmodel
if "__applabel__" not in attrs:
# find the app-label in one of the bases, usually the dbmodel
attrs["__applabel__"] = dbmodel._meta.app_label
if "Meta" not in attrs:
class Meta:
proxy = True
app_label = attrs.get("__applabel__", "typeclasses")
@ -122,9 +150,20 @@ class TypeclassBase(SharedMemoryModelBase):
new_class = ModelBase.__new__(cls, name, bases, attrs)
# django doesn't support inheriting proxy models so we hack support for
# it here by injecting `proxy_for_model` to the actual dbmodel.
# Unfortunately we cannot also set the correct model_name, because this
# would block multiple-inheritance of typeclasses (Django doesn't allow
# multiple bases of the same model).
if dbmodel:
new_class._meta.proxy_for_model = dbmodel
# Maybe Django will eventually handle this in the future:
# new_class._meta.model_name = dbmodel._meta.model_name
# attach signals
signals.post_save.connect(call_at_first_save, sender=new_class)
signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class)
signals.pre_delete.connect(
remove_attributes_on_delete, sender=new_class)
return new_class

View file

@ -9,6 +9,7 @@ be of use when designing your own game.
import os
import gc
import sys
import copy
import types
import math
import re
@ -29,6 +30,8 @@ from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from django.apps import apps
from django.core.validators import validate_email as django_validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
@ -340,14 +343,16 @@ def columnize(string, columns=2, spacing=4, align="l", width=None):
return "\n".join(rows)
def list_to_string(inlist, endsep="and", addquote=False):
def iter_to_string(initer, endsep="and", addquote=False):
"""
This pretty-formats a list as string output, adding an optional
This pretty-formats an iterable list as string output, adding an optional
alternative separator to the second to last entry. If `addquote`
is `True`, the outgoing strings will be surrounded by quotes.
Args:
inlist (list): The list to print.
initer (any): Usually an iterable to print. Each element must be possible to
present with a string. Note that if this is a generator, it will be
consumed by this operation.
endsep (str, optional): If set, the last item separator will
be replaced with this value.
addquote (bool, optional): This will surround all outgoing
@ -372,16 +377,20 @@ def list_to_string(inlist, endsep="and", addquote=False):
endsep = ","
else:
endsep = " " + endsep
if not inlist:
if not initer:
return ""
initer = tuple(str(val) for val in make_iter(initer))
if addquote:
if len(inlist) == 1:
return '"%s"' % inlist[0]
return ", ".join('"%s"' % v for v in inlist[:-1]) + "%s %s" % (endsep, '"%s"' % inlist[-1])
if len(initer) == 1:
return '"%s"' % initer[0]
return ", ".join('"%s"' % v for v in initer[:-1]) + "%s %s" % (endsep, '"%s"' % initer[-1])
else:
if len(inlist) == 1:
return str(inlist[0])
return ", ".join(str(v) for v in inlist[:-1]) + "%s %s" % (endsep, inlist[-1])
if len(initer) == 1:
return str(initer[0])
return ", ".join(str(v) for v in initer[:-1]) + "%s %s" % (endsep, initer[-1])
# legacy alias
list_to_string = iter_to_string
def wildcard_to_regexp(instring):
@ -906,69 +915,25 @@ def to_str(text, session=None):
def validate_email_address(emailaddress):
"""
Checks if an email address is syntactically correct.
Checks if an email address is syntactically correct. Makes use
of the django email-validator for consistency.
Args:
emailaddress (str): Email address to validate.
Returns:
is_valid (bool): If this is a valid email or not.
Notes.
(This snippet was adapted from
http://commandline.org.uk/python/email-syntax-check.)
bool: If this is a valid email or not.
"""
emailaddress = r"%s" % emailaddress
domains = (
"aero",
"asia",
"biz",
"cat",
"com",
"coop",
"edu",
"gov",
"info",
"int",
"jobs",
"mil",
"mobi",
"museum",
"name",
"net",
"org",
"pro",
"tel",
"travel",
)
# Email address must be more than 7 characters in total.
if len(emailaddress) < 7:
return False # Address too short.
# Split up email address into parts.
try:
localpart, domainname = emailaddress.rsplit("@", 1)
host, toplevel = domainname.rsplit(".", 1)
except ValueError:
return False # Address does not have enough parts.
# Check for Country code or Generic Domain.
if len(toplevel) != 2 and toplevel not in domains:
return False # Not a domain name.
for i in "-_.%+.":
localpart = localpart.replace(i, "")
for i in "-_.":
host = host.replace(i, "")
if localpart.isalnum() and host.isalnum():
return True # Email address is fine.
django_validate_email(str(emailaddress))
except DjangoValidationError:
return False
except Exception:
logger.log_trace()
return False
else:
return False # Email address has funny characters.
return True
def inherits_from(obj, parent):

View file

@ -11,10 +11,8 @@ 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 strip_ansi
from evennia.utils.utils import string_partial_matching as _partial
from evennia.utils.utils import string_partial_matching as _partial, validate_email_address
from django.utils.translation import gettext as _
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
@ -214,9 +212,8 @@ def timezone(entry, option_key="Timezone", **kwargs):
def email(entry, option_key="Email Address", **kwargs):
if not entry:
raise ValueError("Email address field empty!")
try:
_val_email(str(entry)) # offloading the hard work to Django!
except _error:
valid = validate_email_address(entry)
if not valid:
raise ValueError(f"That isn't a valid {option_key}!")
return entry