mirror of
https://github.com/evennia/evennia.git
synced 2026-03-19 06:16:31 +01:00
Merge changes from master
This commit is contained in:
commit
68e047dfd2
24 changed files with 324 additions and 244 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
BIN
docs/source/_static/favicon.ico
Normal file
BIN
docs/source/_static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("__"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue