Merge changes from master
This commit is contained in:
commit
94cf728081
24 changed files with 324 additions and 244 deletions
|
|
@ -73,6 +73,9 @@ without arguments starts a full interactive Python console.
|
||||||
required by Django.
|
required by Django.
|
||||||
- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch
|
- 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.
|
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)
|
## 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
|
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.
|
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
|
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
|
operating system. You can also drop into our
|
||||||
[forums](https://groups.google.com/forum/#%21forum/evennia), join `#evennia` on `irc.freenode.net`
|
[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).
|
or chat from the linked [Discord Server](https://discord.gg/NecFePw).
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,10 @@ Twisted packages
|
||||||
## Linux Install
|
## Linux Install
|
||||||
|
|
||||||
If you run into any issues during the installation and first start, please
|
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
|
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
|
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`
|
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
|
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
|
## 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
|
*Applications->Utilities->Terminal*. [Here is an introduction to the Mac
|
||||||
terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line)
|
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
|
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.
|
* 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
|
([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`
|
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
|
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
|
## Windows Install
|
||||||
|
|
||||||
If you run into any issues during the installation, please check out
|
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
|
> If you are running Windows10, consider using the Windows Subsystem for Linux
|
||||||
> ([WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)) instead.
|
> ([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`
|
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
|
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
|
## Where to Go Next
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ $ cd ~ && source evenv/bin/activate
|
||||||
(evenv) $ evennia start
|
(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
|
## 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}.")
|
logger.log_sec(f"Password successfully changed for {self}.")
|
||||||
self.at_password_change()
|
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
|
@classmethod
|
||||||
def create(cls, *args, **kwargs):
|
def create(cls, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -759,31 +804,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||||
logger.log_err(string)
|
logger.log_err(string)
|
||||||
|
|
||||||
if account and settings.MULTISESSION_MODE < 2:
|
if account and settings.MULTISESSION_MODE < 2:
|
||||||
# Load the appropriate Character class
|
# Auto-create a character to go with this account
|
||||||
character_typeclass = kwargs.get(
|
|
||||||
"character_typeclass", settings.BASE_CHARACTER_TYPECLASS
|
|
||||||
)
|
|
||||||
character_home = kwargs.get("home")
|
|
||||||
Character = class_from_module(character_typeclass)
|
|
||||||
|
|
||||||
# Create the character
|
character, errs = account.create_character(typeclass=kwargs.get("character_typeclass"))
|
||||||
character, errs = Character.create(
|
if errs:
|
||||||
account.key,
|
errors.extend(errs)
|
||||||
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
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# We are in the middle between logged in and -not, so we have
|
# We are in the middle between logged in and -not, so we have
|
||||||
|
|
@ -1548,7 +1573,7 @@ class DefaultGuest(DefaultAccount):
|
||||||
try:
|
try:
|
||||||
# Find an available guest name.
|
# Find an available guest name.
|
||||||
for name in settings.GUEST_LIST:
|
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
|
username = name
|
||||||
break
|
break
|
||||||
if not username:
|
if not username:
|
||||||
|
|
@ -1574,6 +1599,15 @@ class DefaultGuest(DefaultAccount):
|
||||||
ip=ip,
|
ip=ip,
|
||||||
)
|
)
|
||||||
errors.extend(errs)
|
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
|
return account, errors
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,26 @@
|
||||||
#
|
#
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
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.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
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.accounts.models import AccountDB
|
||||||
from evennia.typeclasses.admin import AttributeInline, TagInline
|
from evennia.typeclasses.admin import AttributeInline, TagInline
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
|
|
||||||
|
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
|
||||||
|
|
||||||
|
|
||||||
# handle the custom User editor
|
# handle the custom User editor
|
||||||
class AccountDBChangeForm(UserChangeForm):
|
class AccountDBChangeForm(UserChangeForm):
|
||||||
|
|
@ -88,6 +101,7 @@ class AccountForm(forms.ModelForm):
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
model = AccountDB
|
model = AccountDB
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
app_label = "accounts"
|
||||||
|
|
||||||
db_key = forms.RegexField(
|
db_key = forms.RegexField(
|
||||||
label="Username",
|
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):
|
def save_model(self, request, obj, form, change):
|
||||||
"""
|
"""
|
||||||
Custom save actions.
|
Custom save actions.
|
||||||
|
|
|
||||||
|
|
@ -109,8 +109,8 @@ class AccountDB(TypedObject, AbstractUser):
|
||||||
__applabel__ = "accounts"
|
__applabel__ = "accounts"
|
||||||
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
|
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
|
||||||
|
|
||||||
class Meta(object):
|
# class Meta:
|
||||||
verbose_name = "Account"
|
# verbose_name = "Account"
|
||||||
|
|
||||||
# cmdset_storage property
|
# cmdset_storage property
|
||||||
# This seems very sensitive to caching, so leaving it be for now /Griatch
|
# 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
|
tempmergers[prio] = cmdset
|
||||||
|
|
||||||
# sort cmdsets after reverse priority (highest prio are merged in last)
|
# 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
|
# Merge all command sets into one, beginning with the lowest-prio one
|
||||||
cmdset = cmdsets[0]
|
cmdset = sorted_cmdsets[0]
|
||||||
for merging_cmdset in cmdsets[1:]:
|
for merging_cmdset in sorted_cmdsets[1:]:
|
||||||
cmdset = yield cmdset + merging_cmdset
|
cmdset = yield cmdset + merging_cmdset
|
||||||
# store the full sets for diagnosis
|
# store the original, ungrouped set for diagnosis
|
||||||
cmdset.merged_from = cmdsets
|
cmdset.merged_from = cmdsets
|
||||||
# cache
|
# cache
|
||||||
_CMDSET_MERGE_CACHE[mergehash] = cmdset
|
_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)
|
# 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
|
# return the system commands to the cmdset
|
||||||
cmdset_c.add(sys_commands)
|
cmdset_c.add(sys_commands, allow_duplicates=True)
|
||||||
return cmdset_c
|
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*
|
commands or a cmdset to this cmdset. Note that this is *not*
|
||||||
a merge operation (that is handled by the + operator).
|
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
|
cmd (Command, list, Cmdset): This allows for adding one or
|
||||||
more commands to this Cmdset in one go. If another Cmdset
|
more commands to this Cmdset in one go. If another Cmdset
|
||||||
is given, all its commands will be added.
|
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:
|
Notes:
|
||||||
If cmd already exists in set, it will replace the old one
|
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
|
commands[ic] = cmd # replace
|
||||||
except ValueError:
|
except ValueError:
|
||||||
commands.append(cmd)
|
commands.append(cmd)
|
||||||
# extra run to make sure to avoid doublets
|
self.commands = commands
|
||||||
self.commands = list(set(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,
|
# add system_command to separate list as well,
|
||||||
# for quick look-up
|
# for quick look-up
|
||||||
if cmd.key.startswith("__"):
|
if cmd.key.startswith("__"):
|
||||||
|
|
|
||||||
|
|
@ -50,35 +50,6 @@ _UTF8_ERROR = """
|
||||||
Error reported was: '%s'
|
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
|
# Helper functions
|
||||||
|
|
@ -300,42 +271,17 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
|
||||||
"for %s (this might take some time) ..." % python_path
|
"for %s (this might take some time) ..." % python_path
|
||||||
)
|
)
|
||||||
|
|
||||||
procpool = False
|
# run in-process (might block)
|
||||||
if "PythonProcPool" in utils.server_services():
|
for _ in range(len(commands)):
|
||||||
if utils.uses_database("sqlite3"):
|
# loop through the batch file
|
||||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
if not batch_cmd_exec(caller):
|
||||||
else:
|
return
|
||||||
procpool = True
|
step_pointer(caller, 1)
|
||||||
|
# clean out the safety cmdset and clean out all other
|
||||||
if procpool:
|
# temporary attrs.
|
||||||
# run in parallel process
|
string = " Batchfile '%s' applied." % python_path
|
||||||
def callback(r):
|
caller.msg("|G%s" % string)
|
||||||
caller.msg(" |GBatchfile '%s' applied." % python_path)
|
purge_processor(caller)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -420,41 +366,16 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
caller.msg("Running Batch-code processor - Automatic mode for %s ..." % python_path)
|
caller.msg("Running Batch-code processor - Automatic mode for %s ..." % python_path)
|
||||||
|
|
||||||
procpool = False
|
for _ in range(len(codes)):
|
||||||
if "PythonProcPool" in utils.server_services():
|
# loop through the batch file
|
||||||
if utils.uses_database("sqlite3"):
|
if not batch_code_exec(caller):
|
||||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
return
|
||||||
else:
|
step_pointer(caller, 1)
|
||||||
procpool = True
|
# clean out the safety cmdset and clean out all other
|
||||||
if procpool:
|
# temporary attrs.
|
||||||
# run in parallel process
|
string = " Batchfile '%s' applied." % python_path
|
||||||
def callback(r):
|
caller.msg("|G%s" % string)
|
||||||
caller.msg(" |GBatchfile '%s' applied." % python_path)
|
purge_processor(caller)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1407,6 +1407,7 @@ class CmdOpen(ObjManipCommand):
|
||||||
locks = "cmd:perm(open) or perm(Builder)"
|
locks = "cmd:perm(open) or perm(Builder)"
|
||||||
help_category = "Building"
|
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
|
# a custom member method to chug out exits and do checks
|
||||||
def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
|
def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1452,10 +1453,11 @@ class CmdOpen(ObjManipCommand):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# exit does not exist before. Create a new one.
|
# exit does not exist before. Create a new one.
|
||||||
|
lockstring = self.new_obj_lockstring.format(id=caller.id)
|
||||||
if not typeclass:
|
if not typeclass:
|
||||||
typeclass = settings.BASE_EXIT_TYPECLASS
|
typeclass = settings.BASE_EXIT_TYPECLASS
|
||||||
exit_obj = create.create_object(
|
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:
|
if exit_obj:
|
||||||
# storing a destination is what makes it an exit!
|
# storing a destination is what makes it an exit!
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import datetime
|
||||||
from anything import Anything
|
from anything import Anything
|
||||||
|
|
||||||
from django.conf import settings
|
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 import DefaultRoom, DefaultExit, ObjectDB
|
||||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
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):
|
class CommandTest(EvenniaTest):
|
||||||
"""
|
"""
|
||||||
Tests a command
|
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/test2", "Attribute Obj2/test2 = value2")
|
||||||
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.")
|
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")
|
self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
|
||||||
mock_ed.assert_called_with(self.char1, Anything, Anything, key="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: ")
|
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")
|
self.call(building.CmdDesc(), "/edit")
|
||||||
mock_ed.assert_called_with(
|
mock_ed.assert_called_with(
|
||||||
self.char1,
|
self.char1,
|
||||||
|
|
@ -1017,9 +1018,9 @@ class TestBuilding(CommandTest):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
with mock.patch(
|
with patch(
|
||||||
"evennia.commands.default.building.protlib.search_prototype",
|
"evennia.commands.default.building.protlib.search_prototype",
|
||||||
new=mock.MagicMock(return_value=test_prototype),
|
new=MagicMock(return_value=test_prototype),
|
||||||
) as mprot:
|
) as mprot:
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdTypeclass(),
|
building.CmdTypeclass(),
|
||||||
|
|
@ -1085,7 +1086,7 @@ class TestBuilding(CommandTest):
|
||||||
self.call(building.CmdFind(), "/exact Obj", "One Match")
|
self.call(building.CmdFind(), "/exact Obj", "One Match")
|
||||||
|
|
||||||
# Test multitype filtering
|
# Test multitype filtering
|
||||||
with mock.patch(
|
with patch(
|
||||||
"evennia.commands.default.building.CHAR_TYPECLASS",
|
"evennia.commands.default.building.CHAR_TYPECLASS",
|
||||||
"evennia.objects.objects.DefaultCharacter",
|
"evennia.objects.objects.DefaultCharacter",
|
||||||
):
|
):
|
||||||
|
|
@ -1553,11 +1554,11 @@ class TestSystemCommands(CommandTest):
|
||||||
|
|
||||||
self.call(multimatch, "look", "")
|
self.call(multimatch, "look", "")
|
||||||
|
|
||||||
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
|
@patch("evennia.commands.default.syscommands.ChannelDB")
|
||||||
def test_channelcommand(self, mock_channeldb):
|
def test_channelcommand(self, mock_channeldb):
|
||||||
channel = mock.MagicMock()
|
channel = MagicMock()
|
||||||
channel.msg = mock.MagicMock()
|
channel.msg = MagicMock()
|
||||||
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
|
mock_channeldb.objects.get_channel = MagicMock(return_value=channel)
|
||||||
|
|
||||||
self.call(syscommands.SystemSendToChannel(), "public:Hello")
|
self.call(syscommands.SystemSendToChannel(), "public:Hello")
|
||||||
channel.msg.assert_called()
|
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
|
Only true if accessed_obj has the specified tag and optional
|
||||||
category.
|
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):
|
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)
|
elist.append(_("Lock: lock-function '%s' is not available.") % funcstring)
|
||||||
continue
|
continue
|
||||||
args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
|
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))
|
lock_funcs.append((func, args, kwargs))
|
||||||
evalstring = evalstring.replace(funcstring, "%s")
|
evalstring = evalstring.replace(funcstring, "%s")
|
||||||
if len(lock_funcs) < nfuncs:
|
if len(lock_funcs) < nfuncs:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ from evennia.utils import logger
|
||||||
from evennia.utils.utils import make_iter, dbref, lazy_property
|
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
|
Handles and caches the contents of an object to avoid excessive
|
||||||
lookups (this is done very often due to cmdhandler needing to look
|
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.
|
Get all scripts in the database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str, optional): Restrict result to only those
|
key (str or int, optional): Restrict result to only those
|
||||||
with matching key or dbref.
|
with matching key or dbref.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -83,12 +83,9 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
if key:
|
if key:
|
||||||
script = []
|
script = []
|
||||||
dbref = self.dbref(key)
|
dbref = self.dbref(key)
|
||||||
if dbref or dbref == 0:
|
if dbref:
|
||||||
# return either [] or a valid list (never [None])
|
return self.filter(id=dbref)
|
||||||
script = [res for res in [self.dbref_search(dbref)] if res]
|
return self.filter(db_key__iexact=key.strip())
|
||||||
if not script:
|
|
||||||
script = self.filter(db_key=key)
|
|
||||||
return script
|
|
||||||
return self.all()
|
return self.all()
|
||||||
|
|
||||||
def delete_script(self, dbref):
|
def delete_script(self, dbref):
|
||||||
|
|
@ -231,7 +228,7 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
ostring = ostring.strip()
|
ostring = ostring.strip()
|
||||||
|
|
||||||
dbref = self.dbref(ostring)
|
dbref = self.dbref(ostring)
|
||||||
if dbref or dbref == 0:
|
if dbref:
|
||||||
# this is a dbref, try to find the script directly
|
# this is a dbref, try to find the script directly
|
||||||
dbref_match = self.dbref_search(dbref)
|
dbref_match = self.dbref_search(dbref)
|
||||||
if dbref_match and not (
|
if dbref_match and not (
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import time
|
||||||
|
|
||||||
from os.path import dirname, abspath
|
from os.path import dirname, abspath
|
||||||
from twisted.application import internet, service
|
from twisted.application import internet, service
|
||||||
|
from twisted.internet.task import LoopingCall
|
||||||
from twisted.internet import protocol, reactor
|
from twisted.internet import protocol, reactor
|
||||||
from twisted.python.log import ILogObserver
|
from twisted.python.log import ILogObserver
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ import django
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
import evennia
|
import evennia
|
||||||
|
|
||||||
|
|
@ -101,10 +103,29 @@ except ImportError:
|
||||||
WEB_PLUGINS_MODULE = None
|
WEB_PLUGINS_MODULE = None
|
||||||
INFO_DICT["errors"] = (
|
INFO_DICT["errors"] = (
|
||||||
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
|
"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
|
# Portal Service object
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -143,6 +164,9 @@ class Portal(object):
|
||||||
|
|
||||||
self.start_time = time.time()
|
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
|
# in non-interactive portal mode, this gets overwritten by
|
||||||
# cmdline sent by the evennia launcher
|
# cmdline sent by the evennia launcher
|
||||||
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
|
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,8 @@ class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION_CLASS):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
# just to be sure
|
||||||
|
text = to_str(text)
|
||||||
|
|
||||||
flags = self.protocol_flags
|
flags = self.protocol_flags
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Test AMP client
|
||||||
import pickle
|
import pickle
|
||||||
from model_mommy import mommy
|
from model_mommy import mommy
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
from twisted.trial.unittest import TestCase as TwistedTestCase
|
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||||
from evennia.server import amp_client
|
from evennia.server import amp_client
|
||||||
from evennia.server.portal import amp_server
|
from evennia.server.portal import amp_server
|
||||||
|
|
@ -36,6 +36,7 @@ class _TestAMP(TwistedTestCase):
|
||||||
self.server.sessions[1] = self.session
|
self.server.sessions[1] = self.session
|
||||||
|
|
||||||
self.portal = portal.Portal(MagicMock())
|
self.portal = portal.Portal(MagicMock())
|
||||||
|
self.portal.maintenance_task.stop()
|
||||||
self.portalsession = session.Session()
|
self.portalsession = session.Session()
|
||||||
self.portalsession.sessid = 1
|
self.portalsession.sessid = 1
|
||||||
self.portal.sessions[1] = self.portalsession
|
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 django.test.runner import DiscoverRunner
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
class EvenniaTestSuiteRunner(DiscoverRunner):
|
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.
|
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.
|
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
|
import evennia
|
||||||
|
|
||||||
evennia._init()
|
evennia._init()
|
||||||
return super(EvenniaTestSuiteRunner, self).build_suite(
|
return super(EvenniaTestSuiteRunner, self).build_suite(
|
||||||
test_labels, extra_tests=extra_tests, **kwargs
|
test_labels, extra_tests=extra_tests, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,38 @@ class TypeclassBase(SharedMemoryModelBase):
|
||||||
attrs["typename"] = name
|
attrs["typename"] = name
|
||||||
attrs["path"] = "%s.%s" % (attrs["__module__"], name)
|
attrs["path"] = "%s.%s" % (attrs["__module__"], name)
|
||||||
|
|
||||||
# typeclass proxy setup
|
def _get_dbmodel(bases):
|
||||||
if "Meta" not in attrs:
|
"""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
|
proxy = True
|
||||||
app_label = attrs.get("__applabel__", "typeclasses")
|
app_label = attrs.get("__applabel__", "typeclasses")
|
||||||
|
|
||||||
|
|
@ -122,9 +150,20 @@ class TypeclassBase(SharedMemoryModelBase):
|
||||||
|
|
||||||
new_class = ModelBase.__new__(cls, name, bases, attrs)
|
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
|
# attach signals
|
||||||
signals.post_save.connect(call_at_first_save, sender=new_class)
|
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
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ be of use when designing your own game.
|
||||||
import os
|
import os
|
||||||
import gc
|
import gc
|
||||||
import sys
|
import sys
|
||||||
|
import copy
|
||||||
import types
|
import types
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
@ -29,6 +30,8 @@ from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.apps import apps
|
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
|
from evennia.utils import logger
|
||||||
|
|
||||||
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
_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)
|
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`
|
alternative separator to the second to last entry. If `addquote`
|
||||||
is `True`, the outgoing strings will be surrounded by quotes.
|
is `True`, the outgoing strings will be surrounded by quotes.
|
||||||
|
|
||||||
Args:
|
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
|
endsep (str, optional): If set, the last item separator will
|
||||||
be replaced with this value.
|
be replaced with this value.
|
||||||
addquote (bool, optional): This will surround all outgoing
|
addquote (bool, optional): This will surround all outgoing
|
||||||
|
|
@ -372,16 +377,20 @@ def list_to_string(inlist, endsep="and", addquote=False):
|
||||||
endsep = ","
|
endsep = ","
|
||||||
else:
|
else:
|
||||||
endsep = " " + endsep
|
endsep = " " + endsep
|
||||||
if not inlist:
|
if not initer:
|
||||||
return ""
|
return ""
|
||||||
|
initer = tuple(str(val) for val in make_iter(initer))
|
||||||
if addquote:
|
if addquote:
|
||||||
if len(inlist) == 1:
|
if len(initer) == 1:
|
||||||
return '"%s"' % inlist[0]
|
return '"%s"' % initer[0]
|
||||||
return ", ".join('"%s"' % v for v in inlist[:-1]) + "%s %s" % (endsep, '"%s"' % inlist[-1])
|
return ", ".join('"%s"' % v for v in initer[:-1]) + "%s %s" % (endsep, '"%s"' % initer[-1])
|
||||||
else:
|
else:
|
||||||
if len(inlist) == 1:
|
if len(initer) == 1:
|
||||||
return str(inlist[0])
|
return str(initer[0])
|
||||||
return ", ".join(str(v) for v in inlist[:-1]) + "%s %s" % (endsep, inlist[-1])
|
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):
|
def wildcard_to_regexp(instring):
|
||||||
|
|
@ -906,69 +915,25 @@ def to_str(text, session=None):
|
||||||
|
|
||||||
def validate_email_address(emailaddress):
|
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:
|
Args:
|
||||||
emailaddress (str): Email address to validate.
|
emailaddress (str): Email address to validate.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
is_valid (bool): If this is a valid email or not.
|
bool: If this is a valid email or not.
|
||||||
|
|
||||||
Notes.
|
|
||||||
(This snippet was adapted from
|
|
||||||
http://commandline.org.uk/python/email-syntax-check.)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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:
|
try:
|
||||||
localpart, domainname = emailaddress.rsplit("@", 1)
|
django_validate_email(str(emailaddress))
|
||||||
host, toplevel = domainname.rsplit(".", 1)
|
except DjangoValidationError:
|
||||||
except ValueError:
|
return False
|
||||||
return False # Address does not have enough parts.
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
# Check for Country code or Generic Domain.
|
return False
|
||||||
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.
|
|
||||||
else:
|
else:
|
||||||
return False # Email address has funny characters.
|
return True
|
||||||
|
|
||||||
|
|
||||||
def inherits_from(obj, parent):
|
def inherits_from(obj, parent):
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ They can employ more paramters at your leisure.
|
||||||
import re as _re
|
import re as _re
|
||||||
import pytz as _pytz
|
import pytz as _pytz
|
||||||
import datetime as _dt
|
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.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 _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
|
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
|
||||||
|
|
@ -214,9 +212,8 @@ def timezone(entry, option_key="Timezone", **kwargs):
|
||||||
def email(entry, option_key="Email Address", **kwargs):
|
def email(entry, option_key="Email Address", **kwargs):
|
||||||
if not entry:
|
if not entry:
|
||||||
raise ValueError("Email address field empty!")
|
raise ValueError("Email address field empty!")
|
||||||
try:
|
valid = validate_email_address(entry)
|
||||||
_val_email(str(entry)) # offloading the hard work to Django!
|
if not valid:
|
||||||
except _error:
|
|
||||||
raise ValueError(f"That isn't a valid {option_key}!")
|
raise ValueError(f"That isn't a valid {option_key}!")
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue