mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 15:26:30 +01:00
Fix merge conflicts
This commit is contained in:
commit
90b42ca6fb
47 changed files with 1682 additions and 201 deletions
|
|
@ -1,4 +1,5 @@
|
|||
language: python
|
||||
cache: pip
|
||||
python:
|
||||
- "3.6"
|
||||
sudo: false
|
||||
|
|
|
|||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -58,6 +58,15 @@ Web/Django standard initiative (@strikaco)
|
|||
- Bugfixes
|
||||
- Fixes bug on login page where error messages were not being displayed
|
||||
|
||||
### Prototypes
|
||||
|
||||
- `evennia.prototypes.save_prototype` now takes the prototype as a normal
|
||||
argument (`prototype`) instead of having to give it as `**prototype`.
|
||||
- `evennia.prototypes.search_prototype` has a new kwarg `require_single=False` that
|
||||
raises a KeyError exception if query gave 0 or >1 results.
|
||||
- `evennia.prototypes.spawner` can now spawn by passing a `prototype_key`
|
||||
|
||||
|
||||
### Typeclasses
|
||||
|
||||
- Add new methods on all typeclasses, useful specifically for object handling from the website/admin:
|
||||
|
|
@ -81,7 +90,9 @@ Web/Django standard initiative (@strikaco)
|
|||
|
||||
### Utils
|
||||
|
||||
- Added more unit tests.
|
||||
- `evennia` launcher now fully handles all django-admin commands, like running tests in parallel.
|
||||
- `evennia.utils.create.account` now also takes `tags` and `attrs` keywords.
|
||||
- Added many more unit tests.
|
||||
|
||||
### Server
|
||||
|
||||
|
|
@ -213,6 +224,11 @@ Web/Django standard initiative (@strikaco)
|
|||
- `tb_range` - Adds system for abstract positioning and movement.
|
||||
- Updates and some cleanup of existing contribs.
|
||||
|
||||
|
||||
### Internationalization
|
||||
|
||||
- Polish translation by user ogotai
|
||||
|
||||
# Overviews
|
||||
|
||||
## Sept 2017:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ LABEL maintainer="www.evennia.com"
|
|||
|
||||
# install compilation environment
|
||||
RUN apk update && apk add bash gcc jpeg-dev musl-dev procps py-pip \
|
||||
py-setuptools py2-openssl python python-dev zlib-dev
|
||||
py-setuptools py2-openssl python python-dev zlib-dev gettext
|
||||
|
||||
# add the files required for pip installation
|
||||
COPY ./setup.py /usr/src/evennia/
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
"""
|
||||
Evennia MUD/MUX/MU* creation system
|
||||
|
||||
This is the main top-level API for Evennia. You can also explore the
|
||||
evennia library by accessing evennia.<subpackage> directly. From
|
||||
inside the game you can read docs of all object by viewing its
|
||||
`__doc__` string, such as through
|
||||
This is the main top-level API for Evennia. You can explore the evennia library
|
||||
by accessing evennia.<subpackage> directly. From inside the game you can read
|
||||
docs of all object by viewing its `__doc__` string, such as through
|
||||
|
||||
@py evennia.ObjectDB.__doc__
|
||||
|
||||
|
|
@ -18,6 +17,31 @@ See www.evennia.com for full documentation.
|
|||
|
||||
"""
|
||||
|
||||
# docstring header
|
||||
|
||||
DOCSTRING = """
|
||||
|cEvennia|n 'flat' API (use |wevennia.<component>.__doc__|n to read doc-strings
|
||||
and |wdict(evennia.component)|n or
|
||||
|wevennia.component.__dict__ to see contents)
|
||||
|cTypeclass-bases:|n |cDatabase models|n:
|
||||
DefaultAccount DefaultObject AccountDB ObjectDB
|
||||
DefaultGuest DefaultCharacter ChannelDB
|
||||
DefaultRoom ScriptDB
|
||||
DefaultChannel DefaultExit Msg
|
||||
DefaultScript
|
||||
|cSearch functions:|n |cCommand parents and helpers:|n
|
||||
search_account search_object default_cmds
|
||||
search_script search_channel Command InterruptCommand
|
||||
search_help search_message CmdSet
|
||||
search_tag managers |cUtilities:|n
|
||||
|cCreate functions:|n settings lockfuncs
|
||||
create_account create_object logger gametime
|
||||
create_script create_channel ansi spawn
|
||||
create_help_entry create_message contrib managers
|
||||
|cGlobal handlers:|n set_trace
|
||||
TICKER_HANDLER TASK_HANDLER EvMenu EvTable
|
||||
SESSION_HANDLER CHANNEL_HANDLER EvForm EvEditor """
|
||||
|
||||
# Delayed loading of properties
|
||||
|
||||
# Typeclasses
|
||||
|
|
@ -114,7 +138,6 @@ def _create_version():
|
|||
__version__ = _create_version()
|
||||
del _create_version
|
||||
|
||||
|
||||
def _init():
|
||||
"""
|
||||
This function is called automatically by the launcher only after
|
||||
|
|
@ -188,6 +211,10 @@ def _init():
|
|||
from .comms.channelhandler import CHANNEL_HANDLER
|
||||
from .scripts.monitorhandler import MONITOR_HANDLER
|
||||
|
||||
# initialize the doc string
|
||||
global __doc__
|
||||
__doc__ = ansi.parse_ansi(DOCSTRING)
|
||||
|
||||
# API containers
|
||||
|
||||
class _EvContainer(object):
|
||||
|
|
@ -205,15 +232,17 @@ def _init():
|
|||
|
||||
class DBmanagers(_EvContainer):
|
||||
"""
|
||||
Links to instantiated database managers.
|
||||
Links to instantiated Django database managers. These are used
|
||||
to perform more advanced custom database queries than the standard
|
||||
search functions allow.
|
||||
|
||||
helpentry - HelpEntry.objects
|
||||
helpentries - HelpEntry.objects
|
||||
accounts - AccountDB.objects
|
||||
scripts - ScriptDB.objects
|
||||
msgs - Msg.objects
|
||||
channels - Channel.objects
|
||||
objects - ObjectDB.objects
|
||||
serverconfigs = ServerConfig.objects
|
||||
serverconfigs - ServerConfig.objects
|
||||
tags - Tags.objects
|
||||
attributes - Attributes.objects
|
||||
|
||||
|
|
|
|||
|
|
@ -1333,19 +1333,28 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
# list of targets - make list to disconnect from db
|
||||
characters = list(tar for tar in target if tar) if target else []
|
||||
sessions = self.sessions.all()
|
||||
if not sessions:
|
||||
# no sessions, nothing to report
|
||||
return ""
|
||||
is_su = self.is_superuser
|
||||
|
||||
# text shown when looking in the ooc area
|
||||
result = ["Account |g%s|n (you are Out-of-Character)" % self.key]
|
||||
|
||||
nsess = len(sessions)
|
||||
result.append(nsess == 1 and "\n\n|wConnected session:|n" or "\n\n|wConnected sessions (%i):|n" % nsess)
|
||||
result.append(nsess == 1 and
|
||||
"\n\n|wConnected session:|n" or
|
||||
"\n\n|wConnected sessions (%i):|n" % nsess)
|
||||
for isess, sess in enumerate(sessions):
|
||||
csessid = sess.sessid
|
||||
addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple) and
|
||||
str(sess.address[0]) or str(sess.address))
|
||||
result.append("\n %s %s" % (session.sessid == csessid and "|w* %s|n" % (isess + 1) or
|
||||
" %s" % (isess + 1), addr))
|
||||
str(sess.address[0]) or
|
||||
str(sess.address))
|
||||
result.append("\n %s %s" % (
|
||||
session and
|
||||
session.sessid == csessid and
|
||||
"|w* %s|n" % (isess + 1) or
|
||||
" %s" % (isess + 1), addr))
|
||||
result.append("\n\n |whelp|n - more commands")
|
||||
result.append("\n |wooc <Text>|n - talk on public channel")
|
||||
|
||||
|
|
@ -1487,7 +1496,6 @@ class DefaultGuest(DefaultAccount):
|
|||
characters = self.db._playable_characters
|
||||
for character in characters:
|
||||
if character:
|
||||
print("deleting Character:", character)
|
||||
character.delete()
|
||||
|
||||
def at_post_disconnect(self, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from mock import Mock, MagicMock
|
||||
import sys
|
||||
from mock import Mock, MagicMock, patch
|
||||
from random import randint
|
||||
from unittest import TestCase
|
||||
|
||||
|
|
@ -60,19 +61,20 @@ class TestAccountSessionHandler(TestCase):
|
|||
"Check count method"
|
||||
self.assertEqual(self.handler.count(), len(self.handler.get()))
|
||||
|
||||
|
||||
@override_settings(GUEST_ENABLED=True, GUEST_LIST=["bruce_wayne"])
|
||||
class TestDefaultGuest(EvenniaTest):
|
||||
"Check DefaultGuest class"
|
||||
|
||||
ip = '212.216.134.22'
|
||||
|
||||
def test_authenticate(self):
|
||||
|
||||
@override_settings(GUEST_ENABLED=False)
|
||||
def test_create_not_enabled(self):
|
||||
# Guest account should not be permitted
|
||||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||
self.assertFalse(account, 'Guest account was created despite being disabled.')
|
||||
|
||||
settings.GUEST_ENABLED = True
|
||||
settings.GUEST_LIST = ['bruce_wayne']
|
||||
|
||||
def test_authenticate(self):
|
||||
# Create a guest account
|
||||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||
self.assertTrue(account, 'Guest account should have been created.')
|
||||
|
|
@ -81,7 +83,32 @@ class TestDefaultGuest(EvenniaTest):
|
|||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
|
||||
|
||||
settings.GUEST_ENABLED = False
|
||||
@patch("evennia.accounts.accounts.ChannelDB.objects.get_channel")
|
||||
def test_create(self, get_channel):
|
||||
get_channel.connect = MagicMock(return_value=True)
|
||||
account, errors = DefaultGuest.create()
|
||||
self.assertTrue(account, "Guest account should have been created.")
|
||||
self.assertFalse(errors)
|
||||
|
||||
def test_at_post_login(self):
|
||||
self.account.db._last_puppet = self.char1
|
||||
self.account.at_post_login(self.session)
|
||||
self.account.at_post_login()
|
||||
|
||||
def test_at_server_shutdown(self):
|
||||
account, errors = DefaultGuest.create(ip=self.ip)
|
||||
self.char1.delete = MagicMock()
|
||||
account.db._playable_characters = [self.char1]
|
||||
account.at_server_shutdown()
|
||||
self.char1.delete.assert_called()
|
||||
|
||||
def test_at_post_disconnect(self):
|
||||
account, errors = DefaultGuest.create(ip=self.ip)
|
||||
self.char1.delete = MagicMock()
|
||||
account.db._playable_characters = [self.char1]
|
||||
account.at_post_disconnect()
|
||||
self.char1.delete.assert_called()
|
||||
|
||||
|
||||
class TestDefaultAccountAuth(EvenniaTest):
|
||||
|
||||
|
|
@ -162,6 +189,7 @@ class TestDefaultAccountAuth(EvenniaTest):
|
|||
self.assertFalse(account.set_password('Mxyzptlk'))
|
||||
account.delete()
|
||||
|
||||
|
||||
class TestDefaultAccount(TestCase):
|
||||
"Check DefaultAccount class"
|
||||
|
||||
|
|
@ -279,13 +307,83 @@ class TestAccountPuppetDeletion(EvenniaTest):
|
|||
@override_settings(MULTISESSION_MODE=2)
|
||||
def test_puppet_deletion(self):
|
||||
# Check for existing chars
|
||||
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
|
||||
self.assertFalse(self.account.db._playable_characters,
|
||||
'Account should not have any chars by default.')
|
||||
|
||||
# Add char1 to account's playable characters
|
||||
self.account.db._playable_characters.append(self.char1)
|
||||
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
|
||||
self.assertTrue(self.account.db._playable_characters,
|
||||
'Char was not added to account.')
|
||||
|
||||
# See what happens when we delete char1.
|
||||
self.char1.delete()
|
||||
# Playable char list should be empty.
|
||||
self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters)
|
||||
self.assertFalse(self.account.db._playable_characters,
|
||||
'Playable character list is not empty! %s' % self.account.db._playable_characters)
|
||||
|
||||
|
||||
class TestDefaultAccountEv(EvenniaTest):
|
||||
"""
|
||||
Testing using the EvenniaTest parent
|
||||
|
||||
"""
|
||||
def test_characters_property(self):
|
||||
"test existence of None in _playable_characters Attr"
|
||||
self.account.db._playable_characters = [self.char1, None]
|
||||
chars = self.account.characters
|
||||
self.assertEqual(chars, [self.char1])
|
||||
self.assertEqual(self.account.db._playable_characters, [self.char1])
|
||||
|
||||
def test_puppet_success(self):
|
||||
self.account.msg = MagicMock()
|
||||
with patch("evennia.accounts.accounts._MULTISESSION_MODE", 2):
|
||||
self.account.puppet_object(self.session, self.char1)
|
||||
self.account.msg.assert_called_with("You are already puppeting this object.")
|
||||
|
||||
@patch("evennia.accounts.accounts.time.time", return_value=10000)
|
||||
def test_idle_time(self, mock_time):
|
||||
self.session.cmd_last_visible = 10000 - 10
|
||||
idle = self.account.idle_time
|
||||
self.assertEqual(idle, 10)
|
||||
|
||||
# test no sessions
|
||||
with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh:
|
||||
idle = self.account.idle_time
|
||||
self.assertEqual(idle, None)
|
||||
|
||||
@patch("evennia.accounts.accounts.time.time", return_value=10000)
|
||||
def test_connection_time(self, mock_time):
|
||||
self.session.conn_time = 10000 - 10
|
||||
conn = self.account.connection_time
|
||||
self.assertEqual(conn, 10)
|
||||
|
||||
# test no sessions
|
||||
with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh:
|
||||
idle = self.account.connection_time
|
||||
self.assertEqual(idle, None)
|
||||
|
||||
def test_create_account(self):
|
||||
acct = create.account(
|
||||
"TestAccount3", "test@test.com", "testpassword123",
|
||||
locks="test:all()",
|
||||
tags=[("tag1", "category1"), ("tag2", "category2", "data1"), ("tag3", None)],
|
||||
attributes=[("key1", "value1", "category1",
|
||||
"edit:false()", True),
|
||||
("key2", "value2")])
|
||||
acct.save()
|
||||
self.assertTrue(acct.pk)
|
||||
|
||||
def test_at_look(self):
|
||||
ret = self.account.at_look()
|
||||
self.assertTrue("Out-of-Character" in ret)
|
||||
ret = self.account.at_look(target=self.obj1)
|
||||
self.assertTrue("Obj" in ret)
|
||||
ret = self.account.at_look(session=self.session)
|
||||
self.assertTrue("*" in ret) # * marks session is active in list
|
||||
ret = self.account.at_look(target=self.obj1, session=self.session)
|
||||
self.assertTrue("Obj" in ret)
|
||||
ret = self.account.at_look(target="Invalid", session=self.session)
|
||||
self.assertEqual(ret, 'Invalid has no in-game appearance.')
|
||||
|
||||
def test_msg(self):
|
||||
self.account.msg
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,11 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
|
|||
if not target:
|
||||
return
|
||||
|
||||
if target == obj:
|
||||
self.caller.msg("Cannot link an object to itself.")
|
||||
return
|
||||
|
||||
|
||||
string = ""
|
||||
note = "Note: %s(%s) did not have a destination set before. Make sure you linked the right thing."
|
||||
if not obj.destination:
|
||||
|
|
@ -1123,6 +1128,7 @@ class CmdSetHome(CmdLink):
|
|||
|
||||
Usage:
|
||||
@sethome <obj> [= <home_location>]
|
||||
@sethom <obj>
|
||||
|
||||
The "home" location is a "safety" location for objects; they
|
||||
will be moved there if their current location ceases to exist. All
|
||||
|
|
@ -1162,10 +1168,10 @@ class CmdSetHome(CmdLink):
|
|||
old_home = obj.home
|
||||
obj.home = new_home
|
||||
if old_home:
|
||||
string = "%s's home location was changed from %s(%s) to %s(%s)." % (
|
||||
string = "Home location of %s was changed from %s(%s) to %s(%s)." % (
|
||||
obj, old_home, old_home.dbref, new_home, new_home.dbref)
|
||||
else:
|
||||
string = "%s' home location was set to %s(%s)." % (obj, new_home, new_home.dbref)
|
||||
string = "Home location of %s was set to %s(%s)." % (obj, new_home, new_home.dbref)
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -1430,37 +1436,6 @@ def _convert_from_string(cmd, strobj):
|
|||
string this will always fail).
|
||||
"""
|
||||
|
||||
def rec_convert(obj):
|
||||
"""
|
||||
Helper function of recursive conversion calls. This is only
|
||||
used for Python <=2.5. After that literal_eval is available.
|
||||
"""
|
||||
# simple types
|
||||
try:
|
||||
return int(obj)
|
||||
except ValueError:
|
||||
# obj cannot be converted to int - that's fine
|
||||
pass
|
||||
try:
|
||||
return float(obj)
|
||||
except ValueError:
|
||||
# obj cannot be converted to float - that's fine
|
||||
pass
|
||||
# iterables
|
||||
if obj.startswith('[') and obj.endswith(']'):
|
||||
"A list. Traverse recursively."
|
||||
return [rec_convert(val) for val in obj[1:-1].split(',')]
|
||||
if obj.startswith('(') and obj.endswith(')'):
|
||||
"A tuple. Traverse recursively."
|
||||
return tuple([rec_convert(val) for val in obj[1:-1].split(',')])
|
||||
if obj.startswith('{') and obj.endswith('}') and ':' in obj:
|
||||
"A dict. Traverse recursively."
|
||||
return dict([(rec_convert(pair.split(":", 1)[0]),
|
||||
rec_convert(pair.split(":", 1)[1]))
|
||||
for pair in obj[1:-1].split(',') if ":" in pair])
|
||||
# if nothing matches, return as-is
|
||||
return obj
|
||||
|
||||
# Use literal_eval to parse python structure exactly.
|
||||
try:
|
||||
return _LITERAL_EVAL(strobj)
|
||||
|
|
@ -1471,10 +1446,9 @@ def _convert_from_string(cmd, strobj):
|
|||
"Make sure this is acceptable." % strobj
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
else:
|
||||
# fall back to old recursive solution (does not support
|
||||
# nested lists/dicts)
|
||||
return rec_convert(strobj.strip())
|
||||
except Exception as err:
|
||||
string = "|RUnknown error in evaluating Attribute: {}".format(err)
|
||||
return string
|
||||
|
||||
|
||||
class CmdSetAttribute(ObjManipCommand):
|
||||
|
|
@ -1957,7 +1931,7 @@ class CmdLock(ObjManipCommand):
|
|||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
string = "@lock <object>[ = <lockstring>] or @lock[/switch] " \
|
||||
string = "Usage: @lock <object>[ = <lockstring>] or @lock[/switch] " \
|
||||
"<object>/<access_type>"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
|
@ -1978,8 +1952,8 @@ class CmdLock(ObjManipCommand):
|
|||
caller.msg("You need 'control' access to change this type of lock.")
|
||||
return
|
||||
|
||||
if not has_control_access or obj.access(caller, "edit"):
|
||||
caller.msg("You are not allowed to do that.")
|
||||
if not has_control_access or not obj.access(caller, "edit"):
|
||||
caller.msg("You need 'edit' access to view or delete lock on this object.")
|
||||
return
|
||||
|
||||
lockdef = obj.locks.get(access_type)
|
||||
|
|
@ -2753,11 +2727,11 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
obj)
|
||||
else:
|
||||
# no tag specified, clear all tags
|
||||
old_tags = ["%s%s" % (tag, " (category: %s" % category if category else "")
|
||||
old_tags = ["%s%s" % (tag, " (category: %s)" % category if category else "")
|
||||
for tag, category in obj.tags.all(return_key_and_category=True)]
|
||||
if old_tags:
|
||||
obj.tags.clear()
|
||||
string = "Cleared all tags from %s: %s" % (obj, ", ".join(old_tags))
|
||||
string = "Cleared all tags from %s: %s" % (obj, ", ".join(sorted(old_tags)))
|
||||
else:
|
||||
string = "No Tags to clear on %s." % obj
|
||||
self.caller.msg(string)
|
||||
|
|
@ -2788,8 +2762,9 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
tags = [tup[0] for tup in tagtuples]
|
||||
categories = [" (category: %s)" % tup[1] if tup[1] else "" for tup in tagtuples]
|
||||
if ntags:
|
||||
string = "Tag%s on %s: %s" % ("s" if ntags > 1 else "", obj,
|
||||
", ".join("'%s'%s" % (tags[i], categories[i]) for i in range(ntags)))
|
||||
string = "Tag%s on %s: %s" % (
|
||||
"s" if ntags > 1 else "", obj,
|
||||
", ".join(sorted("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))))
|
||||
else:
|
||||
string = "No tags attached to %s." % obj
|
||||
self.caller.msg(string)
|
||||
|
|
@ -3002,7 +2977,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# all seems ok. Try to save.
|
||||
try:
|
||||
prot = protlib.save_prototype(**prototype)
|
||||
prot = protlib.save_prototype(prototype)
|
||||
if not prot:
|
||||
caller.msg("|rError saving:|R {}.|n".format(prototype_key))
|
||||
return
|
||||
|
|
@ -3091,7 +3066,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
elif nprots > 1:
|
||||
caller.msg("Found {} prototypes matching '{}':\n {}".format(
|
||||
nprots, prototype, ", ".join(prot.get('prototype_key', '')
|
||||
nprots, prototype, ", ".join(proto.get('prototype_key', '')
|
||||
for proto in prototypes)))
|
||||
return
|
||||
# we have a prototype, check access
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ System commands
|
|||
|
||||
import traceback
|
||||
import os
|
||||
import io
|
||||
import datetime
|
||||
import sys
|
||||
import django
|
||||
|
|
@ -21,7 +22,7 @@ from evennia.accounts.models import AccountDB
|
|||
from evennia.utils import logger, utils, gametime, create, search
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.utils import crop, class_from_module
|
||||
from evennia.utils.utils import crop, class_from_module, to_unicode
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
|
|
@ -197,6 +198,7 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
|
|||
duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000)
|
||||
else:
|
||||
ret = eval(pycode_compiled, {}, available_vars)
|
||||
|
||||
if mode == "eval":
|
||||
ret = "%s%s" % (str(ret), duration)
|
||||
else:
|
||||
|
|
@ -238,7 +240,9 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
inherits_from(obj, parent) : check object inheritance
|
||||
|
||||
You can explore The evennia API from inside the game by calling
|
||||
evennia.help(), evennia.managers.help() etc.
|
||||
the `__doc__` property on entities:
|
||||
@py evennia.__doc__
|
||||
@py evennia.managers.__doc__
|
||||
|
||||
|rNote: In the wrong hands this command is a severe security risk.
|
||||
It should only be accessible by trusted server admins/superusers.|n
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ main test suite started with
|
|||
> python game/manage.py test.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import types
|
||||
import datetime
|
||||
from anything import Anything
|
||||
|
||||
from django.conf import settings
|
||||
from mock import Mock, mock
|
||||
|
|
@ -49,7 +49,7 @@ class CommandTest(EvenniaTest):
|
|||
Tests a command
|
||||
"""
|
||||
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
|
||||
receiver=None, cmdstring=None, obj=None, inputs=None):
|
||||
receiver=None, cmdstring=None, obj=None, inputs=None, raw_string=None):
|
||||
"""
|
||||
Test a command by assigning all the needed
|
||||
properties to cmdobj and running
|
||||
|
|
@ -74,7 +74,7 @@ class CommandTest(EvenniaTest):
|
|||
cmdobj.cmdset = cmdset
|
||||
cmdobj.session = SESSIONS.session_from_sessid(1)
|
||||
cmdobj.account = self.account
|
||||
cmdobj.raw_string = cmdobj.key + " " + args
|
||||
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + args
|
||||
cmdobj.obj = obj or (caller if caller else self.char1)
|
||||
# test
|
||||
old_msg = receiver.msg
|
||||
|
|
@ -322,30 +322,90 @@ class TestBuilding(CommandTest):
|
|||
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
|
||||
self.call(building.CmdCreate(), "/d TestObj1", # /d switch is abbreviated form of /drop
|
||||
"You create a new %s: TestObj1." % name)
|
||||
self.call(building.CmdCreate(), "", "Usage: ")
|
||||
self.call(building.CmdCreate(), "TestObj1;foo;bar",
|
||||
"You create a new %s: TestObj1 (aliases: foo, bar)." % name)
|
||||
|
||||
def test_examine(self):
|
||||
self.call(building.CmdExamine(), "", "Name/key: Room")
|
||||
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
||||
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
||||
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
|
||||
|
||||
self.char1.db.test = "testval"
|
||||
self.call(building.CmdExamine(), "self/test", "Persistent attributes:\n test = testval")
|
||||
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
|
||||
self.call(building.CmdExamine(), "out", "Name/key: out")
|
||||
|
||||
self.room1.scripts.add(self.script.__class__)
|
||||
self.call(building.CmdExamine(), "")
|
||||
self.account.scripts.add(self.script.__class__)
|
||||
self.call(building.CmdExamine(), "*TestAccount")
|
||||
|
||||
def test_set_obj_alias(self):
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj(#4)")
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
|
||||
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to 'testobj1b'.")
|
||||
self.call(building.CmdSetObjAlias(), "", "Usage: ")
|
||||
self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")
|
||||
self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj(#4): 'testobj1b'")
|
||||
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
|
||||
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
|
||||
|
||||
def test_copy(self):
|
||||
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b",
|
||||
"Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
|
||||
self.call(building.CmdCopy(), "", "Usage: ")
|
||||
self.call(building.CmdCopy(), "Obj", "Identical copy of Obj, named 'Obj_copy' was created.")
|
||||
self.call(building.CmdCopy(), "NotFound = Foo", "Could not find 'NotFound'.")
|
||||
|
||||
def test_attribute_commands(self):
|
||||
self.call(building.CmdSetAttribute(), "", "Usage: ")
|
||||
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
||||
self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created 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'.")
|
||||
|
||||
with mock.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')
|
||||
|
||||
self.call(building.CmdSetAttribute(), "Obj2/test3=\"value3\"", "Created attribute Obj2/test3 = 'value3'")
|
||||
self.call(building.CmdSetAttribute(), "Obj2/test3 = ", "Deleted attribute 'test3' (= True) from Obj2.")
|
||||
|
||||
self.call(building.CmdCpAttr(), "/copy Obj2/test2 = Obj2/test3",
|
||||
"@cpattr: Extra switch \"/copy\" ignored.|\nCopied Obj2.test2 -> Obj2.test3. "
|
||||
"(value: 'value2')")
|
||||
self.call(building.CmdMvAttr(), "", "Usage: ")
|
||||
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
|
||||
self.call(building.CmdCpAttr(), "", "Usage: ")
|
||||
self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3")
|
||||
|
||||
self.call(building.CmdWipe(), "", "Usage: ")
|
||||
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
|
||||
self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.")
|
||||
|
||||
def test_name(self):
|
||||
self.call(building.CmdName(), "", "Usage: ")
|
||||
self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")
|
||||
self.call(building.CmdName(), "*TestAccount=TestAccountRenamed",
|
||||
"Account's name changed to 'TestAccountRenamed'.")
|
||||
self.call(building.CmdName(), "*NotFound=TestAccountRenamed",
|
||||
"Could not find '*NotFound'")
|
||||
self.call(building.CmdName(), "Obj3=Obj4;foo;bar",
|
||||
"Object's name changed to 'Obj4' (foo, bar).")
|
||||
self.call(building.CmdName(), "Obj4=", "No names or aliases defined!")
|
||||
|
||||
def test_desc(self):
|
||||
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
|
||||
self.call(building.CmdDesc(), "", "Usage: ")
|
||||
|
||||
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
||||
self.call(building.CmdDesc(), "/edit")
|
||||
mock_ed.assert_called_with(self.char1, key='desc',
|
||||
loadfunc=building._desc_load,
|
||||
quitfunc=building._desc_quit,
|
||||
savefunc=building._desc_save,
|
||||
persistent=True)
|
||||
|
||||
def test_empty_desc(self):
|
||||
"""
|
||||
|
|
@ -365,50 +425,128 @@ class TestBuilding(CommandTest):
|
|||
assert self.obj2.db.desc == o2d
|
||||
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
|
||||
|
||||
def test_wipe(self):
|
||||
def test_destroy(self):
|
||||
confirm = building.CmdDestroy.confirm
|
||||
building.CmdDestroy.confirm = False
|
||||
self.call(building.CmdDestroy(), "", "Usage: ")
|
||||
self.call(building.CmdDestroy(), "Obj", "Obj was destroyed.")
|
||||
self.call(building.CmdDestroy(), "Obj", "Obj2 was destroyed.")
|
||||
self.call(building.CmdDestroy(), "Obj", "Could not find 'Obj'.| (Objects to destroy "
|
||||
"must either be local or specified with a unique #dbref.)")
|
||||
self.call(building.CmdDestroy(), "#1", "You are trying to delete") # DEFAULT_HOME
|
||||
self.char2.location = self.room2
|
||||
self.call(building.CmdDestroy(), self.room2.dbref,
|
||||
"Char2(#7) arrives to Room(#1) from Room2(#2).|Room2 was destroyed.")
|
||||
building.CmdDestroy.confirm = confirm
|
||||
|
||||
def test_destroy_sequence(self):
|
||||
confirm = building.CmdDestroy.confirm
|
||||
building.CmdDestroy.confirm = False
|
||||
self.call(building.CmdDestroy(),
|
||||
"{}-{}".format(self.obj1.dbref, self.obj2.dbref),
|
||||
"Obj was destroyed.\nObj2 was destroyed.")
|
||||
|
||||
def test_dig(self):
|
||||
self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
|
||||
self.call(building.CmdDig(), "", "Usage: ")
|
||||
|
||||
def test_tunnel(self):
|
||||
self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
|
||||
self.call(building.CmdTunnel(), "", "Usage: ")
|
||||
self.call(building.CmdTunnel(), "foo = TestRoom2;test2", "@tunnel can only understand the")
|
||||
self.call(building.CmdTunnel(), "/tel e = TestRoom3;test3",
|
||||
"Created room TestRoom3(#11) (test3) of type typeclasses.rooms.Room.\n"
|
||||
"Created Exit from Room to TestRoom3: east(#12) (e).\n"
|
||||
"Created Exit back from TestRoom3 to Room: west(#13) (w).|TestRoom3(#11)")
|
||||
|
||||
def test_tunnel_exit_typeclass(self):
|
||||
self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3", "Created room TestRoom3")
|
||||
self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3",
|
||||
"Created room TestRoom3")
|
||||
|
||||
def test_exit_commands(self):
|
||||
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
|
||||
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
|
||||
self.call(building.CmdUnLink(), "", "Usage: ")
|
||||
self.call(building.CmdLink(), "NotFound", "Could not find 'NotFound'.")
|
||||
self.call(building.CmdLink(), "TestExit", "TestExit1 is an exit to Room.")
|
||||
self.call(building.CmdLink(), "Obj", "Obj is not an exit. Its home location is Room.")
|
||||
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
||||
|
||||
self.char1.location = self.room2
|
||||
self.call(building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room.")
|
||||
self.call(building.CmdOpen(), "TestExit2=Room", "Exit TestExit2 already exists. It already points to the correct place.")
|
||||
|
||||
|
||||
# ensure it matches locally first
|
||||
self.call(building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way).")
|
||||
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.exit.dbref),
|
||||
"Link created TestExit2 (in Room2) <-> out (in Room) (two-way).")
|
||||
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.room1.dbref),
|
||||
"To create a two-way link, TestExit2 and Room must both have a location ")
|
||||
self.call(building.CmdLink(), "/twoway {}={}".format(self.exit.dbref, self.exit.dbref),
|
||||
"Cannot link an object to itself.")
|
||||
self.call(building.CmdLink(), "", "Usage: ")
|
||||
# ensure can still match globally when not a local name
|
||||
self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1(#8) did not have a destination set before. "
|
||||
"Make sure you linked the right thing.\n"
|
||||
"Link created TestExit1 -> Room2 (one way).")
|
||||
self.call(building.CmdLink(), "TestExit1=", "Former exit TestExit1 no longer links anywhere.")
|
||||
|
||||
def test_set_home(self):
|
||||
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")
|
||||
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was changed from Room")
|
||||
self.call(building.CmdSetHome(), "", "Usage: ")
|
||||
self.call(building.CmdSetHome(), "self", "Char's current home is Room")
|
||||
self.call(building.CmdSetHome(), "Obj", "Obj's current home is Room2")
|
||||
self.obj1.home = None
|
||||
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
||||
|
||||
def test_list_cmdsets(self):
|
||||
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
||||
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
|
||||
|
||||
def test_typeclass(self):
|
||||
self.call(building.CmdTypeclass(), "", "Usage: ")
|
||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit.")
|
||||
self.call(building.CmdTypeclass(), "Obj2 = evennia.objects.objects.DefaultExit",
|
||||
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit.", cmdstring="@swap")
|
||||
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
|
||||
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultExit'")
|
||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to override.")
|
||||
self.call(building.CmdTypeclass(), "/force Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj updated its existing typeclass ")
|
||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultObject")
|
||||
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultObject'")
|
||||
self.call(building.CmdTypeclass(), "Obj",
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
|
||||
"Only the at_object_creation hook was run (update mode). Attributes set before swap were not removed.",
|
||||
cmdstring="@update")
|
||||
self.call(building.CmdTypeclass(), "/reset/force Obj=evennia.objects.objects.DefaultObject",
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
|
||||
"All object creation hooks were run. All old attributes where deleted before the swap.")
|
||||
|
||||
def test_lock(self):
|
||||
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
|
||||
self.call(building.CmdLock(), "", "Usage: ")
|
||||
self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")
|
||||
self.call(building.CmdLock(), "*TestAccount = test:all()", "Added lock 'test:all()' to TestAccount")
|
||||
self.call(building.CmdLock(), "Obj/notfound", "Obj has no lock of access type 'notfound'.")
|
||||
self.call(building.CmdLock(), "Obj/test", "test:all()")
|
||||
self.call(building.CmdLock(), "/view Obj = edit:false()",
|
||||
"Switch(es) view can not be used with a lock assignment. "
|
||||
"Use e.g. @lock/del objname/locktype instead.")
|
||||
self.call(building.CmdLock(), "Obj = edit:false()")
|
||||
self.call(building.CmdLock(), "Obj/test", "You need 'edit' access to view or delete lock on this object.")
|
||||
self.call(building.CmdLock(), "Obj", "call:true()") # etc
|
||||
self.call(building.CmdLock(), "*TestAccount", "boot:perm(Admin)") # etc
|
||||
|
||||
def test_find(self):
|
||||
self.call(building.CmdFind(), "", "Usage: ")
|
||||
self.call(building.CmdFind(), "oom2", "One Match")
|
||||
self.call(building.CmdFind(), "oom2 = 1-100", "One Match")
|
||||
self.call(building.CmdFind(), "oom2 = 1 100", "One Match") # space works too
|
||||
expect = "One Match(#1-#7, loc):\n " +\
|
||||
"Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))"
|
||||
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate")
|
||||
|
|
@ -420,11 +558,43 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find")
|
||||
self.call(building.CmdFind(), "/startswith Room2", "One Match")
|
||||
|
||||
self.call(building.CmdFind(), self.char1.dbref,
|
||||
"Exact dbref match(#1-#7):\n "
|
||||
" Char(#6) - evennia.objects.objects.DefaultCharacter")
|
||||
self.call(building.CmdFind(), "*TestAccount",
|
||||
"Match(#1-#7):\n"
|
||||
" TestAccount - evennia.accounts.accounts.DefaultAccount")
|
||||
|
||||
self.call(building.CmdFind(), "/char Obj")
|
||||
self.call(building.CmdFind(), "/room Obj")
|
||||
self.call(building.CmdFind(), "/exit Obj")
|
||||
self.call(building.CmdFind(), "/exact Obj", "One Match")
|
||||
|
||||
def test_script(self):
|
||||
self.call(building.CmdScript(), "Obj = ", "No scripts defined on Obj")
|
||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||
self.call(building.CmdScript(), "", "Usage: ")
|
||||
self.call(building.CmdScript(), "= Obj", "To create a global script you need @scripts/add <typeclass>.")
|
||||
self.call(building.CmdScript(), "Obj = ", "dbref obj")
|
||||
|
||||
self.call(building.CmdScript(), "/start Obj", "0 scripts started on Obj") # because it's already started
|
||||
self.call(building.CmdScript(), "/stop Obj", "Stopping script")
|
||||
|
||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||
self.call(building.CmdScript(), "/start Obj = scripts.Script", "Script scripts.Script could not be (re)started.")
|
||||
self.call(building.CmdScript(), "/stop Obj = scripts.Script", "Script stopped and removed from object.")
|
||||
|
||||
def test_teleport(self):
|
||||
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.")
|
||||
self.call(building.CmdTeleport(), "", "Usage: ")
|
||||
self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
|
||||
self.call(building.CmdTeleport(), "Obj = NotFound", "Could not find 'NotFound'.|Destination not found.")
|
||||
self.call(building.CmdTeleport(),
|
||||
"Obj = Room2", "Obj(#4) is leaving Room(#1), heading for Room2(#2).|Teleported Obj -> Room2.")
|
||||
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
|
||||
self.call(building.CmdTeleport(), "Obj = Obj", "You can't teleport an object inside of itself!")
|
||||
|
||||
self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
|
||||
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)")
|
||||
self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone
|
||||
"Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.")
|
||||
self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc
|
||||
|
|
@ -432,6 +602,28 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet
|
||||
"Char is already at Room2.")
|
||||
|
||||
def test_tag(self):
|
||||
self.call(building.CmdTag(), "", "Usage: ")
|
||||
|
||||
self.call(building.CmdTag(), "Obj = testtag")
|
||||
self.call(building.CmdTag(), "Obj = testtag2")
|
||||
self.call(building.CmdTag(), "Obj = testtag2:category1")
|
||||
self.call(building.CmdTag(), "Obj = testtag3")
|
||||
|
||||
self.call(building.CmdTag(), "Obj", "Tags on Obj: 'testtag', 'testtag2', "
|
||||
"'testtag2' (category: category1), 'testtag3'")
|
||||
|
||||
self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.")
|
||||
self.call(building.CmdTag(), "/search testtag", "Found 1 object with tag 'testtag':")
|
||||
self.call(building.CmdTag(), "/search testtag2", "Found 1 object with tag 'testtag2':")
|
||||
self.call(building.CmdTag(), "/search testtag2:category1",
|
||||
"Found 1 object with tag 'testtag2' (category: 'category1'):")
|
||||
|
||||
|
||||
self.call(building.CmdTag(), "/del Obj = testtag3", "Removed tag 'testtag3' from Obj.")
|
||||
self.call(building.CmdTag(), "/del Obj",
|
||||
"Cleared all tags from Obj: testtag, testtag2, testtag2 (category: category1)")
|
||||
|
||||
def test_spawn(self):
|
||||
def getObject(commandTest, objKeyStr):
|
||||
# A helper function to get a spawned object and
|
||||
|
|
@ -453,6 +645,14 @@ class TestBuilding(CommandTest):
|
|||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"Saved prototype: testprot", inputs=['y'])
|
||||
|
||||
self.call(building.CmdSpawn(), "/search ", "Key ")
|
||||
self.call(building.CmdSpawn(), "/search test;test2", "")
|
||||
|
||||
self.call(building.CmdSpawn(),
|
||||
"/save {'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"To save a prototype it must have the 'prototype_key' set.")
|
||||
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
|
||||
|
|
@ -480,9 +680,9 @@ class TestBuilding(CommandTest):
|
|||
goblin.delete()
|
||||
|
||||
# create prototype
|
||||
protlib.create_prototype(**{'key': 'Ball',
|
||||
'typeclass': 'evennia.objects.objects.DefaultCharacter',
|
||||
'prototype_key': 'testball'})
|
||||
protlib.create_prototype({'key': 'Ball',
|
||||
'typeclass': 'evennia.objects.objects.DefaultCharacter',
|
||||
'prototype_key': 'testball'})
|
||||
|
||||
# Tests "@spawn <prototype_name>"
|
||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def display_meter(cur_value, max_value,
|
|||
|
||||
# Pick which fill color to use based on how full the bar is
|
||||
fillcolor_index = (float(len(fill_color)) * percent_full)
|
||||
fillcolor_index = int(round(fillcolor_index)) - 1
|
||||
fillcolor_index = max(0, int(round(fillcolor_index)) - 1)
|
||||
fillcolor_code = "|[" + fill_color[fillcolor_index]
|
||||
|
||||
# Make color codes for empty bar portion and text_color
|
||||
|
|
|
|||
|
|
@ -2,17 +2,18 @@
|
|||
Module containing the test cases for the Audit system.
|
||||
"""
|
||||
|
||||
from django.test import override_settings
|
||||
from django.conf import settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
import re
|
||||
|
||||
# Configure session auditing settings
|
||||
# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests
|
||||
settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog"
|
||||
settings.AUDIT_IN = True
|
||||
settings.AUDIT_OUT = True
|
||||
settings.AUDIT_ALLOW_SPARSE = True
|
||||
|
||||
# Configure settings to use custom session
|
||||
# Configure settings to use custom session - TODO: This is bad practice, changing global settings
|
||||
settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -691,8 +691,21 @@ from evennia.contrib import health_bar
|
|||
|
||||
class TestHealthBar(EvenniaTest):
|
||||
def test_healthbar(self):
|
||||
expected_bar_str = "|[G|w |n|[B|w test24 / 200test |n"
|
||||
self.assertTrue(health_bar.display_meter(24, 200, length=40, pre_text="test", post_text="test", align="center") == expected_bar_str)
|
||||
expected_bar_str = '|[R|w|n|[B|w test0 / 200test |n'
|
||||
self.assertEqual(health_bar.display_meter(
|
||||
0, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str)
|
||||
expected_bar_str = "|[R|w |n|[B|w test24 / 200test |n"
|
||||
self.assertEqual(health_bar.display_meter(
|
||||
24, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str)
|
||||
expected_bar_str = '|[Y|w test100 /|n|[B|w 200test |n'
|
||||
self.assertEqual(health_bar.display_meter(
|
||||
100, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str)
|
||||
expected_bar_str = '|[G|w test180 / 200test |n|[B|w |n'
|
||||
self.assertEqual(health_bar.display_meter(
|
||||
180, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str)
|
||||
expected_bar_str = '|[G|w test200 / 200test |n|[B|w|n'
|
||||
self.assertEqual(health_bar.display_meter(
|
||||
200, 200, length=40, pre_text="test", post_text="test", align="center"), expected_bar_str)
|
||||
|
||||
# test mail contrib
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ start
|
|||
#
|
||||
# This room inherits from a Typeclass called WeatherRoom. It regularly
|
||||
# and randomly shows some weather effects. Note how we can spread the
|
||||
# command's arguments over more than one line for easy reading. we
|
||||
# command's arguments over more than one line for easy reading. We
|
||||
# also make sure to create plenty of aliases for the room and
|
||||
# exits. Note the alias tut#02: this unique identifier can be used
|
||||
# later in the script to always find the way back to this room (for
|
||||
|
|
@ -420,10 +420,6 @@ north
|
|||
|
||||
(to get a weapon from the barrel, use |wget weapon|n)
|
||||
#
|
||||
@desc barrel =
|
||||
This barrel has the air of leftovers - it contains an assorted
|
||||
mess of random weaponry in various states and qualities.
|
||||
#
|
||||
@detail barkeep;man;landlord =
|
||||
The landlord is a cheerful fellow, always ready to supply you with
|
||||
more beer. He mentions doing some sort of arcane magic known as
|
||||
|
|
@ -439,8 +435,17 @@ north
|
|||
#
|
||||
@create/drop barrel: tutorial_world.objects.WeaponRack
|
||||
#
|
||||
@desc barrel =
|
||||
This barrel has the air of leftovers - it contains an assorted
|
||||
mess of random weaponry in various states and qualities.
|
||||
#
|
||||
@lock barrel = get:false()
|
||||
#
|
||||
# Players trying to pickup barrel will receive hint to 'get weapon' instead
|
||||
#
|
||||
@set barrel/get_err_msg =
|
||||
The barkeep shakes his head. He says: 'Get weapon, not the barrel.'
|
||||
#
|
||||
# This id makes sure that we cannot pick more than one weapon from this rack
|
||||
#
|
||||
@set barrel/rack_id = "rack_barrel"
|
||||
|
|
@ -836,7 +841,7 @@ archway
|
|||
#
|
||||
# Set its home to this location
|
||||
#
|
||||
@home ghost = tut#11
|
||||
@sethome ghost = tut#11
|
||||
#
|
||||
@lock ghost = get:false()
|
||||
#
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CmdMobOnOff(Command):
|
|||
to turn on/off the mob."
|
||||
"""
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: mobon|moboff <mob>")
|
||||
self.caller.msg("Usage: mobon||moboff <mob>")
|
||||
return
|
||||
mob = self.caller.search(self.args)
|
||||
if not mob:
|
||||
|
|
|
|||
BIN
evennia/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
evennia/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
315
evennia/locale/pl/LC_MESSAGES/django.po
Normal file
315
evennia/locale/pl/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# Polish localization for the EVENNIA.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <akroczyk@gmail.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ArkMUD Polish translation v0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-02-20 12:13+0000\n"
|
||||
"PO-Revision-Date: 2019-02-20 14:18+0100\n"
|
||||
"Language: pl_PL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
|
||||
"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
|
||||
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: ArkMUD team\n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
|
||||
#: accounts/accounts.py:440
|
||||
msgid "Account being deleted."
|
||||
msgstr "Konto zostalo usuniete."
|
||||
|
||||
#: commands/cmdhandler.py:681
|
||||
msgid "There were multiple matches."
|
||||
msgstr "Znaleziono wiele dopasowan."
|
||||
|
||||
#: commands/cmdhandler.py:704
|
||||
#, python-format
|
||||
msgid "Command '%s' is not available."
|
||||
msgstr "Komenda '%s' jests niedostepna."
|
||||
|
||||
#: commands/cmdhandler.py:709
|
||||
#, python-format
|
||||
msgid " Maybe you meant %s?"
|
||||
msgstr " Czy miales na mysli %s?"
|
||||
|
||||
#: commands/cmdhandler.py:709
|
||||
msgid "or"
|
||||
msgstr "lub"
|
||||
|
||||
#: commands/cmdhandler.py:711
|
||||
msgid " Type \"help\" for help."
|
||||
msgstr " Wpisz \"help\" aby otworzyc pomoc."
|
||||
|
||||
#: commands/cmdsethandler.py:89
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"Error loading cmdset '{path}'\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:94
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:98
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"SyntaxError encountered when loading cmdset '{path}'.\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:103
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"Compile/Run error when loading cmdset '{path}'.\",\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:108
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"Error encountered for cmdset at path '{path}'.\n"
|
||||
"Replacing with fallback '{fallback_path}'.\n"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:114
|
||||
#, python-brace-format
|
||||
msgid "Fallback path '{fallback_path}' failed to generate a cmdset."
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:182 commands/cmdsethandler.py:192
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"(Unsuccessfully tried '%s')."
|
||||
msgstr ""
|
||||
"\n"
|
||||
"(Bezskuteczna proba '%s')."
|
||||
|
||||
#: commands/cmdsethandler.py:311
|
||||
#, python-brace-format
|
||||
msgid "custom {mergetype} on cmdset '{cmdset}'"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:314
|
||||
#, python-brace-format
|
||||
msgid " <Merged {mergelist} {mergetype}, prio {prio}>: {current}"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:322
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
" <{key} ({mergetype}, prio {prio}, {permstring})>:\n"
|
||||
" {keylist}"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:426
|
||||
msgid "Only CmdSets can be added to the cmdsethandler!"
|
||||
msgstr ""
|
||||
|
||||
#: comms/channelhandler.py:100
|
||||
msgid "Say what?"
|
||||
msgstr "Ze co?"
|
||||
|
||||
#: comms/channelhandler.py:105
|
||||
#, python-format
|
||||
msgid "Channel '%s' not found."
|
||||
msgstr "Kanal '%s' nie odnaleziony."
|
||||
|
||||
#: comms/channelhandler.py:108
|
||||
#, python-format
|
||||
msgid "You are not connected to channel '%s'."
|
||||
msgstr "Nie jestes polaczony z kanalem '%s'."
|
||||
|
||||
#: comms/channelhandler.py:112
|
||||
#, python-format
|
||||
msgid "You are not permitted to send to channel '%s'."
|
||||
msgstr "Nie masz uprawnien do wysylania wiadomosci na kanal '%s'."
|
||||
|
||||
#: comms/channelhandler.py:155
|
||||
msgid " (channel)"
|
||||
msgstr " (kanal)"
|
||||
|
||||
#: locks/lockhandler.py:236
|
||||
#, python-format
|
||||
msgid "Lock: lock-function '%s' is not available."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:249
|
||||
#, python-format
|
||||
msgid "Lock: definition '%s' has syntax errors."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:253
|
||||
#, python-format
|
||||
msgid ""
|
||||
"LockHandler on %(obj)s: access type '%(access_type)s' changed from "
|
||||
"'%(source)s' to '%(goal)s' "
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:320
|
||||
#, python-brace-format
|
||||
msgid "Lock: '{lockdef}' contains no colon (:)."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:328
|
||||
#, python-brace-format
|
||||
msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:336
|
||||
#, python-brace-format
|
||||
msgid "Lock: '{lockdef}' has mismatched parentheses."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:343
|
||||
#, python-brace-format
|
||||
msgid "Lock: '{lockdef}' has no valid lock functions."
|
||||
msgstr ""
|
||||
|
||||
#: objects/objects.py:732
|
||||
#, python-format
|
||||
msgid "Couldn't perform move ('%s'). Contact an admin."
|
||||
msgstr "Nie udalo sie wykonac ruchu ('%s'). Skontaktuj sie z adminem."
|
||||
|
||||
#: objects/objects.py:742
|
||||
msgid "The destination doesn't exist."
|
||||
msgstr "Punkt przeznaczenia nie istnieje."
|
||||
|
||||
#: objects/objects.py:833
|
||||
#, python-format
|
||||
msgid "Could not find default home '(#%d)'."
|
||||
msgstr "Nie znaleziono domyslnego domu '(#%d)'."
|
||||
|
||||
#: objects/objects.py:849
|
||||
msgid "Something went wrong! You are dumped into nowhere. Contact an admin."
|
||||
msgstr "Cos poszlo zle! Zostales wrzucony w nicosc. Skontaktuj sie z adminem."
|
||||
|
||||
#: objects/objects.py:915
|
||||
#, python-format
|
||||
msgid "Your character %s has been destroyed."
|
||||
msgstr "Twoja postac %s zostala zniszczona."
|
||||
|
||||
#: scripts/scripthandler.py:53
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/scripts.py:205
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'."
|
||||
msgstr ""
|
||||
|
||||
#: server/initial_setup.py:28
|
||||
msgid ""
|
||||
"\n"
|
||||
"Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if "
|
||||
"you need\n"
|
||||
"help, want to contribute, report issues or just join the community.\n"
|
||||
"As Account #1 you can create a demo/tutorial area with |w@batchcommand "
|
||||
"tutorial_world.build|n.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Witaj w swojej nowej grze, bazujacej na |wEvennia|n! Odwiedz http://www."
|
||||
"evennia.com\n"
|
||||
"jesli potrzebujesz pomocy, chcesz pomoc badz zglosic blad, lub po prostu "
|
||||
"chcesz dolaczyc do spolecznosci.\n"
|
||||
"Jako Konto #1 mozesz otworzyc demo/samouczek wpisujac |w@batchcommand "
|
||||
"tutorial_world.build|n.\n"
|
||||
" "
|
||||
|
||||
#: server/initial_setup.py:92
|
||||
msgid "This is User #1."
|
||||
msgstr "To jest User #1."
|
||||
|
||||
#: server/initial_setup.py:105
|
||||
msgid "Limbo"
|
||||
msgstr "Otchlan"
|
||||
|
||||
#: server/server.py:139
|
||||
msgid "idle timeout exceeded"
|
||||
msgstr "czas bezczynnosci przekroczony"
|
||||
|
||||
#: server/sessionhandler.py:386
|
||||
msgid " ... Server restarted."
|
||||
msgstr " ... Serwer zrestartowany."
|
||||
|
||||
#: server/sessionhandler.py:606
|
||||
msgid "Logged in from elsewhere. Disconnecting."
|
||||
msgstr "Zalogowano z innego miejsca. Rozlaczanie."
|
||||
|
||||
#: server/sessionhandler.py:634
|
||||
msgid "Idle timeout exceeded, disconnecting."
|
||||
msgstr "Czas bezczynnosci przekroczony, rozlaczanie."
|
||||
|
||||
#: server/validators.py:50
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%s From a terminal client, you can also use a phrase of multiple words if "
|
||||
"you enclose the password in double quotes."
|
||||
msgstr ""
|
||||
"%s Z poziomu terminala, mozesz rowniez uzyc frazy z wieloma slowami jesli "
|
||||
"ujmiesz haslo w cudzyslowie."
|
||||
|
||||
#: utils/evmenu.py:192
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Menu node '{nodename}' is either not implemented or caused an error. Make "
|
||||
"another choice."
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:194
|
||||
#, python-brace-format
|
||||
msgid "Error in menu node '{nodename}'."
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:195
|
||||
msgid "No description."
|
||||
msgstr "Brak opisu."
|
||||
|
||||
#: utils/evmenu.py:196
|
||||
msgid "Commands: <menu option>, help, quit"
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:197
|
||||
msgid "Commands: <menu option>, help"
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:198
|
||||
msgid "Commands: help, quit"
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:199
|
||||
msgid "Commands: help"
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:200
|
||||
msgid "Choose an option or try 'help'."
|
||||
msgstr "Wybierz opcje lub uzyj komendy 'help'."
|
||||
|
||||
#: utils/utils.py:1866
|
||||
#, python-format
|
||||
msgid "Could not find '%s'."
|
||||
msgstr "Nie odnaleziono '%s'."
|
||||
|
||||
#: utils/utils.py:1873
|
||||
#, python-format
|
||||
msgid "More than one match for '%s' (please narrow target):\n"
|
||||
msgstr "Wiecej niz jedno dopasowanie dla '%s' (prosze zawezyc cel):\n"
|
||||
|
|
@ -69,7 +69,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
object candidates.
|
||||
|
||||
Return:
|
||||
match (Object or list): One or more matching results.
|
||||
match (query): Matching query.
|
||||
|
||||
"""
|
||||
ostring = str(ostring).lstrip('*')
|
||||
|
|
@ -77,7 +77,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
dbref = self.dbref(ostring)
|
||||
if dbref:
|
||||
try:
|
||||
return self.get(id=dbref)
|
||||
return self.get(db_account__id=dbref)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
|
@ -87,13 +87,13 @@ class ObjectDBManager(TypedObjectManager):
|
|||
if exact:
|
||||
return self.filter(cand_restriction & Q(db_account__username__iexact=ostring))
|
||||
else: # fuzzy matching
|
||||
ply_cands = self.filter(cand_restriction & Q(accountdb__username__istartswith=ostring)
|
||||
).values_list("db_key", flat=True)
|
||||
if candidates:
|
||||
index_matches = string_partial_matching(ply_cands, ostring, ret_index=True)
|
||||
return [obj for ind, obj in enumerate(make_iter(candidates)) if ind in index_matches]
|
||||
else:
|
||||
return string_partial_matching(ply_cands, ostring, ret_index=False)
|
||||
obj_cands = self.select_related().filter(cand_restriction & Q(db_account__username__istartswith=ostring))
|
||||
acct_cands = [obj.account for obj in obj_cands]
|
||||
|
||||
if obj_cands:
|
||||
index_matches = string_partial_matching([acct.key for acct in acct_cands], ostring, ret_index=True)
|
||||
acct_cands = [acct_cands[i].id for i in index_matches]
|
||||
return obj_cands.filter(db_account__id__in=acct_cands)
|
||||
|
||||
def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None):
|
||||
"""
|
||||
|
|
@ -105,7 +105,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
candidates (list, optional): Only match among the given list of candidates.
|
||||
|
||||
Returns:
|
||||
matches (list): The matching objects.
|
||||
matches (query): The matching objects.
|
||||
"""
|
||||
cand_restriction = candidates is not None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates)
|
||||
if obj]) or Q()
|
||||
|
|
@ -119,18 +119,19 @@ class ObjectDBManager(TypedObjectManager):
|
|||
|
||||
Args:
|
||||
attribute_name (str): Attribute name to search for.
|
||||
candidates (list, optional): Only match among the given list of candidates.
|
||||
candidates (list, optional): Only match among the given list of object
|
||||
candidates.
|
||||
|
||||
Returns:
|
||||
matches (list): All objects having the given attribute_name defined at all.
|
||||
matches (query): All objects having the given attribute_name defined at all.
|
||||
|
||||
"""
|
||||
cand_restriction = candidates is not None and Q(db_attributes__db_obj__pk__in=[_GA(obj, "id") for obj
|
||||
in make_iter(candidates)
|
||||
if obj]) or Q()
|
||||
return list(self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name)))
|
||||
cand_restriction = \
|
||||
candidates is not None and Q(id__in=[obj.id for obj in candidates]) or Q()
|
||||
return self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name))
|
||||
|
||||
def get_objs_with_attr_value(self, attribute_name, attribute_value, candidates=None, typeclasses=None):
|
||||
def get_objs_with_attr_value(self, attribute_name, attribute_value,
|
||||
candidates=None, typeclasses=None):
|
||||
"""
|
||||
Get all objects having the given attrname set to the given value.
|
||||
|
||||
|
|
@ -141,7 +142,8 @@ class ObjectDBManager(TypedObjectManager):
|
|||
typeclasses (list, optional): Python pats to restrict matches with.
|
||||
|
||||
Returns:
|
||||
matches (list): Objects fullfilling both the `attribute_name` and `attribute_value` criterions.
|
||||
matches (list): Objects fullfilling both the `attribute_name` and
|
||||
`attribute_value` criterions.
|
||||
|
||||
Notes:
|
||||
This uses the Attribute's PickledField to transparently search the database by matching
|
||||
|
|
|
|||
|
|
@ -262,7 +262,13 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
|||
con = self.contents_cache.get(exclude=exclude)
|
||||
# print "contents_get:", self, con, id(self), calledby() # DEBUG
|
||||
return con
|
||||
contents = property(contents_get)
|
||||
|
||||
def contents_set(self, *args):
|
||||
"You cannot replace this property"
|
||||
raise AttributeError("{}.contents is read-only. Use obj.move_to or "
|
||||
"obj.location to move an object here.".format(self.__class__))
|
||||
|
||||
contents = property(contents_get, contents_set, contents_set)
|
||||
|
||||
@property
|
||||
def exits(self):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
|
||||
class DefaultObjectTest(EvenniaTest):
|
||||
|
|
@ -45,3 +46,48 @@ class DefaultObjectTest(EvenniaTest):
|
|||
|
||||
self.assertTrue(self.room1.get_absolute_url())
|
||||
self.assertTrue('admin' in self.room1.web_get_admin_url())
|
||||
|
||||
|
||||
class TestObjectManager(EvenniaTest):
|
||||
"Test object manager methods"
|
||||
def test_get_object_with_account(self):
|
||||
query = ObjectDB.objects.get_object_with_account("TestAccount").first()
|
||||
self.assertEqual(query, self.char1)
|
||||
query = ObjectDB.objects.get_object_with_account(self.account.dbref)
|
||||
self.assertEqual(query, self.char1)
|
||||
query = ObjectDB.objects.get_object_with_account("#123456")
|
||||
self.assertFalse(query)
|
||||
query = ObjectDB.objects.get_object_with_account("TestAccou").first()
|
||||
self.assertFalse(query)
|
||||
|
||||
query = ObjectDB.objects.get_object_with_account("TestAccou", exact=False)
|
||||
self.assertEqual(tuple(query), (self.char1, self.char2))
|
||||
|
||||
query = ObjectDB.objects.get_object_with_account(
|
||||
"TestAccou", candidates=[self.char1, self.obj1], exact=False)
|
||||
self.assertEqual(list(query), [self.char1])
|
||||
|
||||
def test_get_objs_with_key_and_typeclass(self):
|
||||
query = ObjectDB.objects.get_objs_with_key_and_typeclass(
|
||||
"Char", "evennia.objects.objects.DefaultCharacter")
|
||||
self.assertEqual(list(query), [self.char1])
|
||||
query = ObjectDB.objects.get_objs_with_key_and_typeclass(
|
||||
"Char", "evennia.objects.objects.DefaultObject")
|
||||
self.assertFalse(query)
|
||||
query = ObjectDB.objects.get_objs_with_key_and_typeclass(
|
||||
"NotFound", "evennia.objects.objects.DefaultCharacter")
|
||||
self.assertFalse(query)
|
||||
query = ObjectDB.objects.get_objs_with_key_and_typeclass(
|
||||
"Char", "evennia.objects.objects.DefaultCharacter", candidates=[self.char1, self.char2])
|
||||
self.assertEqual(list(query), [self.char1])
|
||||
|
||||
def test_get_objs_with_attr(self):
|
||||
self.obj1.db.testattr = "testval1"
|
||||
query = ObjectDB.objects.get_objs_with_attr("testattr")
|
||||
self.assertEqual(list(query), [self.obj1])
|
||||
query = ObjectDB.objects.get_objs_with_attr(
|
||||
"testattr", candidates=[self.char1, self.obj1] )
|
||||
self.assertEqual(list(query), [self.obj1])
|
||||
query = ObjectDB.objects.get_objs_with_attr(
|
||||
"NotFound", candidates=[self.char1, self.obj1] )
|
||||
self.assertFalse(query)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ from evennia import prototypes
|
|||
|
||||
goblin = {"prototype_key": "goblin:, ... }
|
||||
|
||||
prototype = prototypes.save_prototype(caller, **goblin)
|
||||
prototype = prototypes.save_prototype(goblin)
|
||||
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -2139,7 +2139,7 @@ def node_prototype_save(caller, **kwargs):
|
|||
# we already validated and accepted the save, so this node acts as a goto callback and
|
||||
# should now only return the next node
|
||||
prototype_key = prototype.get("prototype_key")
|
||||
protlib.save_prototype(**prototype)
|
||||
protlib.save_prototype(prototype)
|
||||
|
||||
spawned_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||
nspawned = spawned_objects.count()
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ def objlist(*args, **kwargs):
|
|||
def dbref(*args, **kwargs):
|
||||
"""
|
||||
Usage $dbref(<#dbref>)
|
||||
Returns one Object searched globally by #dbref. Error if #dbref is invalid.
|
||||
Validate that a #dbref input is valid.
|
||||
"""
|
||||
if not args or len(args) < 1 or _RE_DBREF.match(args[0]) is None:
|
||||
raise ValueError('$dbref requires a valid #dbref argument.')
|
||||
|
|
|
|||
|
|
@ -147,13 +147,13 @@ class DbPrototype(DefaultScript):
|
|||
# Prototype manager functions
|
||||
|
||||
|
||||
def save_prototype(**kwargs):
|
||||
def save_prototype(prototype):
|
||||
"""
|
||||
Create/Store a prototype persistently.
|
||||
|
||||
Kwargs:
|
||||
prototype_key (str): This is required for any storage.
|
||||
All other kwargs are considered part of the new prototype dict.
|
||||
Args:
|
||||
prototype (dict): The prototype to save. A `prototype_key` key is
|
||||
required.
|
||||
|
||||
Returns:
|
||||
prototype (dict or None): The prototype stored using the given kwargs, None if deleting.
|
||||
|
|
@ -166,8 +166,8 @@ def save_prototype(**kwargs):
|
|||
is expected to have valid permissions.
|
||||
|
||||
"""
|
||||
|
||||
kwargs = homogenize_prototype(kwargs)
|
||||
in_prototype = prototype
|
||||
in_prototype = homogenize_prototype(in_prototype)
|
||||
|
||||
def _to_batchtuple(inp, *args):
|
||||
"build tuple suitable for batch-creation"
|
||||
|
|
@ -176,7 +176,7 @@ def save_prototype(**kwargs):
|
|||
return inp
|
||||
return (inp, ) + args
|
||||
|
||||
prototype_key = kwargs.get("prototype_key")
|
||||
prototype_key = in_prototype.get("prototype_key")
|
||||
if not prototype_key:
|
||||
raise ValidationError("Prototype requires a prototype_key")
|
||||
|
||||
|
|
@ -192,21 +192,21 @@ def save_prototype(**kwargs):
|
|||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||
prototype = stored_prototype[0].prototype if stored_prototype else {}
|
||||
|
||||
kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", ""))
|
||||
prototype_locks = kwargs.get(
|
||||
in_prototype['prototype_desc'] = in_prototype.get("prototype_desc", prototype.get("prototype_desc", ""))
|
||||
prototype_locks = in_prototype.get(
|
||||
"prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)"))
|
||||
is_valid, err = validate_lockstring(prototype_locks)
|
||||
if not is_valid:
|
||||
raise ValidationError("Lock error: {}".format(err))
|
||||
kwargs['prototype_locks'] = prototype_locks
|
||||
in_prototype['prototype_locks'] = prototype_locks
|
||||
|
||||
prototype_tags = [
|
||||
_to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY)
|
||||
for tag in make_iter(kwargs.get("prototype_tags",
|
||||
for tag in make_iter(in_prototype.get("prototype_tags",
|
||||
prototype.get('prototype_tags', [])))]
|
||||
kwargs["prototype_tags"] = prototype_tags
|
||||
in_prototype["prototype_tags"] = prototype_tags
|
||||
|
||||
prototype.update(kwargs)
|
||||
prototype.update(in_prototype)
|
||||
|
||||
if stored_prototype:
|
||||
# edit existing prototype
|
||||
|
|
@ -261,19 +261,25 @@ def delete_prototype(prototype_key, caller=None):
|
|||
return True
|
||||
|
||||
|
||||
def search_prototype(key=None, tags=None):
|
||||
def search_prototype(key=None, tags=None, require_single=False):
|
||||
"""
|
||||
Find prototypes based on key and/or tags, or all prototypes.
|
||||
|
||||
Kwargs:
|
||||
key (str): An exact or partial key to query for.
|
||||
tags (str or list): Tag key or keys to query for. These
|
||||
tags (str or list): Tag key or keys to query for. These
|
||||
will always be applied with the 'db_protototype'
|
||||
tag category.
|
||||
require_single (bool): If set, raise KeyError if the result
|
||||
was not found or if there are multiple matches.
|
||||
|
||||
Return:
|
||||
matches (list): All found prototype dicts. If no keys
|
||||
or tags are given, all available prototypes will be returned.
|
||||
matches (list): All found prototype dicts. Empty list if
|
||||
no match was found. Note that if neither `key` nor `tags`
|
||||
were given, *all* available prototypes will be returned.
|
||||
|
||||
Raises:
|
||||
KeyError: If `require_single` is True and there are 0 or >1 matches.
|
||||
|
||||
Note:
|
||||
The available prototypes is a combination of those supplied in
|
||||
|
|
@ -329,6 +335,10 @@ def search_prototype(key=None, tags=None):
|
|||
if mta.get('prototype_key') and mta['prototype_key'] == key]
|
||||
if filter_matches and len(filter_matches) < nmatches:
|
||||
matches = filter_matches
|
||||
|
||||
nmatches = len(matches)
|
||||
if nmatches != 1 and require_single:
|
||||
raise KeyError("Found {} matching prototypes.".format(nmatches))
|
||||
|
||||
return matches
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ prot = {
|
|||
"attrs": [("weapon", "sword")]
|
||||
}
|
||||
|
||||
prot = prototypes.create_prototype(**prot)
|
||||
prot = prototypes.create_prototype(prot)
|
||||
|
||||
```
|
||||
|
||||
|
|
@ -662,8 +662,9 @@ def spawn(*prototypes, **kwargs):
|
|||
Spawn a number of prototyped objects.
|
||||
|
||||
Args:
|
||||
prototypes (dict): Each argument should be a prototype
|
||||
dictionary.
|
||||
prototypes (str or dict): Each argument should either be a
|
||||
prototype_key (will be used to find the prototype) or a full prototype
|
||||
dictionary. These will be batched-spawned as one object each.
|
||||
Kwargs:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
|
|
@ -673,8 +674,10 @@ def spawn(*prototypes, **kwargs):
|
|||
prototype_parents (dict): A dictionary holding a custom
|
||||
prototype-parent dictionary. Will overload same-named
|
||||
prototypes from prototype_modules.
|
||||
return_parents (bool): Only return a dict of the
|
||||
prototype-parents (no object creation happens)
|
||||
return_parents (bool): Return a dict of the entire prototype-parent tree
|
||||
available to this prototype (no object creation happens). This is a
|
||||
merged result between the globally found protparents and whatever
|
||||
custom `prototype_parents` are given to this function.
|
||||
only_validate (bool): Only run validation of prototype/parents
|
||||
(no object creation) and return the create-kwargs.
|
||||
|
||||
|
|
@ -684,6 +687,11 @@ def spawn(*prototypes, **kwargs):
|
|||
`return_parents` is set, instead return dict of prototype parents.
|
||||
|
||||
"""
|
||||
# search string (=prototype_key) from input
|
||||
prototypes = [protlib.search_prototype(prot, require_single=True)[0]
|
||||
if isinstance(prot, basestring) else prot
|
||||
for prot in prototypes]
|
||||
|
||||
# get available protparents
|
||||
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TestSpawner(EvenniaTest):
|
|||
self.prot1 = {"prototype_key": "testprototype",
|
||||
"typeclass": "evennia.objects.objects.DefaultObject"}
|
||||
|
||||
def test_spawn(self):
|
||||
def test_spawn_from_prot(self):
|
||||
obj1 = spawner.spawn(self.prot1)
|
||||
# check spawned objects have the right tag
|
||||
self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1)
|
||||
|
|
@ -62,6 +62,14 @@ class TestSpawner(EvenniaTest):
|
|||
_PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"],
|
||||
prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard'])
|
||||
|
||||
def test_spawn_from_str(self):
|
||||
protlib.save_prototype(self.prot1)
|
||||
obj1 = spawner.spawn(self.prot1['prototype_key'])
|
||||
self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1)
|
||||
self.assertEqual([o.key for o in spawner.spawn(
|
||||
_PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"],
|
||||
prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard'])
|
||||
|
||||
|
||||
class TestUtils(EvenniaTest):
|
||||
|
||||
|
|
@ -245,6 +253,7 @@ class TestProtLib(EvenniaTest):
|
|||
super(TestProtLib, self).setUp()
|
||||
self.obj1.attributes.add("testattr", "testval")
|
||||
self.prot = spawner.prototype_from_object(self.obj1)
|
||||
|
||||
|
||||
def test_prototype_to_str(self):
|
||||
prstr = protlib.prototype_to_str(self.prot)
|
||||
|
|
@ -253,6 +262,22 @@ class TestProtLib(EvenniaTest):
|
|||
def test_check_permission(self):
|
||||
pass
|
||||
|
||||
def test_save_prototype(self):
|
||||
result = protlib.save_prototype(self.prot)
|
||||
self.assertEqual(result, self.prot)
|
||||
# faulty
|
||||
self.prot['prototype_key'] = None
|
||||
self.assertRaises(protlib.ValidationError, protlib.save_prototype, self.prot)
|
||||
|
||||
def test_search_prototype(self):
|
||||
protlib.save_prototype(self.prot)
|
||||
match = protlib.search_prototype("NotFound")
|
||||
self.assertFalse(match)
|
||||
match = protlib.search_prototype()
|
||||
self.assertTrue(match)
|
||||
match = protlib.search_prototype(self.prot['prototype_key'])
|
||||
self.assertEqual(match, [self.prot])
|
||||
|
||||
|
||||
@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20)
|
||||
class TestProtFuncs(EvenniaTest):
|
||||
|
|
@ -424,7 +449,7 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
def test_prototype_storage(self):
|
||||
|
||||
# from evennia import set_trace;set_trace(term_size=(180, 50))
|
||||
prot1 = protlib.create_prototype(**self.prot1)
|
||||
prot1 = protlib.create_prototype(self.prot1)
|
||||
|
||||
self.assertTrue(bool(prot1))
|
||||
self.assertEqual(prot1, self.prot1)
|
||||
|
|
@ -436,7 +461,7 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
protlib.DbPrototype.objects.get_by_tag(
|
||||
"foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1)
|
||||
|
||||
prot2 = protlib.create_prototype(**self.prot2)
|
||||
prot2 = protlib.create_prototype(self.prot2)
|
||||
self.assertEqual(
|
||||
[pobj.db.prototype
|
||||
for pobj in protlib.DbPrototype.objects.get_by_tag(
|
||||
|
|
@ -445,7 +470,7 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
|
||||
# add to existing prototype
|
||||
prot1b = protlib.create_prototype(
|
||||
prototype_key='testprototype1', foo='bar', prototype_tags=['foo2'])
|
||||
{"prototype_key": 'testprototype1', "foo": 'bar', "prototype_tags": ['foo2']})
|
||||
|
||||
self.assertEqual(
|
||||
[pobj.db.prototype
|
||||
|
|
@ -457,7 +482,7 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1])
|
||||
self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b])
|
||||
|
||||
prot3 = protlib.create_prototype(**self.prot3)
|
||||
prot3 = protlib.create_prototype(self.prot3)
|
||||
|
||||
# partial match
|
||||
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||
|
|
@ -608,7 +633,7 @@ class TestMenuModule(EvenniaTest):
|
|||
self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something)
|
||||
self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"])
|
||||
|
||||
protlib.save_prototype(**self.test_prot)
|
||||
protlib.save_prototype(self.test_prot)
|
||||
|
||||
# locks helpers
|
||||
self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Run the script with the -h flag to see usage information.
|
|||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
import importlib
|
||||
|
|
@ -25,6 +26,7 @@ from subprocess import Popen, check_output, call, CalledProcessError, STDOUT
|
|||
from twisted.protocols import amp
|
||||
from twisted.internet import reactor, endpoints
|
||||
import django
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
# Signal processing
|
||||
SIG = signal.SIGINT
|
||||
|
|
@ -393,6 +395,7 @@ ERROR_DJANGO_MIN = \
|
|||
ERROR: Django {dversion} found. Evennia requires version {django_min}
|
||||
or higher.
|
||||
|
||||
TE_TEST
|
||||
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
|
||||
`evennia` is the folder to where you cloned the Evennia library. If not
|
||||
in a virtualenv you can install django with for example `pip install --upgrade django`
|
||||
|
|
@ -428,16 +431,15 @@ NOTE_KEYBOARDINTERRUPT = \
|
|||
NOTE_TEST_DEFAULT = \
|
||||
"""
|
||||
TESTING: Using Evennia's default settings file (evennia.settings_default).
|
||||
(use 'evennia --settings settings.py test .' to run tests on the game dir)
|
||||
(use 'evennia test --settings settings.py .' to run only your custom game tests)
|
||||
"""
|
||||
|
||||
NOTE_TEST_CUSTOM = \
|
||||
"""
|
||||
TESTING: Using specified settings file '{settings_dotpath}'.
|
||||
|
||||
(Obs: Evennia's full test suite may not pass if the settings are very
|
||||
different from the default. Use 'test .' as arguments to run only tests
|
||||
on the game dir.)
|
||||
OBS: Evennia's full test suite may not pass if the settings are very
|
||||
different from the default (use 'evennia test evennia' to run core tests)
|
||||
"""
|
||||
|
||||
PROCESS_ERROR = \
|
||||
|
|
@ -519,7 +521,7 @@ def _print_info(portal_info_dict, server_info_dict):
|
|||
out = {}
|
||||
for key, value in dct.items():
|
||||
if isinstance(value, list):
|
||||
value = "\n{}".format(ind).join(value)
|
||||
value = "\n{}".format(ind).join(str(val) for val in value)
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
|
@ -686,13 +688,14 @@ def send_instruction(operation, arguments, callback=None, errback=None):
|
|||
|
||||
if AMP_CONNECTION:
|
||||
# already connected - send right away
|
||||
_send()
|
||||
return _send()
|
||||
else:
|
||||
# we must connect first, send once connected
|
||||
point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT)
|
||||
deferred = endpoints.connectProtocol(point, AMPLauncherProtocol())
|
||||
deferred.addCallbacks(_on_connect, _on_connect_fail)
|
||||
REACTOR_RUN = True
|
||||
return deferred
|
||||
|
||||
|
||||
def query_status(callback=None):
|
||||
|
|
@ -2116,7 +2119,7 @@ def main():
|
|||
else:
|
||||
kill(SERVER_PIDFILE, 'Server')
|
||||
elif option != "noop":
|
||||
# pass-through to django manager
|
||||
# pass-through to django manager, but set things up first
|
||||
check_db = False
|
||||
need_gamedir = True
|
||||
# some commands don't require the presence of a game directory to work
|
||||
|
|
@ -2136,31 +2139,16 @@ def main():
|
|||
|
||||
init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir)
|
||||
|
||||
# pass on to the manager
|
||||
args = [option]
|
||||
kwargs = {}
|
||||
if unknown_args:
|
||||
for arg in unknown_args:
|
||||
if arg.startswith("--"):
|
||||
print("arg:", arg)
|
||||
if "=" in arg:
|
||||
arg, value = [p.strip() for p in arg.split("=", 1)]
|
||||
else:
|
||||
value = True
|
||||
kwargs[arg.lstrip("--")] = value
|
||||
else:
|
||||
args.append(arg)
|
||||
|
||||
# makemessages needs a special syntax to not conflict with the -l option
|
||||
if len(args) > 1 and args[0] == "makemessages":
|
||||
args.insert(1, "-l")
|
||||
|
||||
try:
|
||||
django.core.management.call_command(*args, **kwargs)
|
||||
except django.core.management.base.CommandError as exc:
|
||||
args = ", ".join(args)
|
||||
kwargs = ", ".join(["--%s" % kw for kw in kwargs])
|
||||
print(ERROR_INPUT.format(traceback=exc, args=args, kwargs=kwargs))
|
||||
if option == "migrate":
|
||||
# we have to launch migrate within the program to make sure migrations
|
||||
# run within the scope of the launcher (otherwise missing a db will cause errors)
|
||||
django.core.management.call_command(*([option] + unknown_args))
|
||||
else:
|
||||
# pass on to the core django manager - re-parse the entire input line
|
||||
# but keep 'evennia' as the name instead of django-admin. This is
|
||||
# an exit condition.
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(execute_from_command_line())
|
||||
|
||||
elif not args.tail_log:
|
||||
# no input; print evennia info (don't pring if we're tailing log)
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
self.send_mode = True
|
||||
self.send_task = None
|
||||
self.multibatches = 0
|
||||
# later twisted amp has its own __init__
|
||||
super(AMPMultiConnectionProtocol, self).__init__(*args, **kwargs)
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ class Portal(object):
|
|||
sys.path.append('.')
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
self.services = service.MultiService()
|
||||
self.services.setServiceParent(application)
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = PORTAL_SESSIONS
|
||||
self.sessions.portal = self
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class TestMemPlot(TestCase):
|
|||
@patch.object(memplot, "os")
|
||||
@patch.object(memplot, "open", new_callable=mock_open, create=True)
|
||||
@patch.object(memplot, "time")
|
||||
@patch("evennia.utils.idmapper.models.SharedMemoryModel.flush_from_cache", new=Mock())
|
||||
def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper):
|
||||
if isinstance(memplot, Mock):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -143,8 +143,6 @@ def _server_maintenance():
|
|||
session.account.access(session.account, "noidletimeout", default=False):
|
||||
SESSIONS.disconnect(session, reason=reason)
|
||||
|
||||
maintenance_task = LoopingCall(_server_maintenance)
|
||||
maintenance_task.start(60, now=True) # call every minute
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Main Server object
|
||||
|
|
@ -199,6 +197,7 @@ class Evennia(object):
|
|||
reactor.callLater(1, d.callback, None)
|
||||
reactor.sigInt = _wrap_sigint_handler
|
||||
|
||||
|
||||
# Server startup methods
|
||||
|
||||
def sqlite3_prep(self):
|
||||
|
|
@ -303,6 +302,10 @@ class Evennia(object):
|
|||
"""
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
# start server time and maintenance task
|
||||
self.maintenance_task = LoopingCall(_server_maintenance)
|
||||
self.maintenance_task.start(60, now=True) # call every minute
|
||||
|
||||
# update eventual changed defaults
|
||||
self.update_defaults()
|
||||
|
||||
|
|
@ -322,6 +325,7 @@ class Evennia(object):
|
|||
# clear eventual lingering session storages
|
||||
ObjectDB.objects.clear_all_sessids()
|
||||
logger.log_msg("Evennia Server successfully started.")
|
||||
|
||||
# always call this regardless of start type
|
||||
self.at_server_start()
|
||||
|
||||
|
|
|
|||
126
evennia/server/tests/test_amp_connection.py
Normal file
126
evennia/server/tests/test_amp_connection.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
Test AMP client
|
||||
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from unittest import TestCase
|
||||
from 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
|
||||
from evennia.server.portal import amp
|
||||
from evennia.server import server
|
||||
from evennia.server.portal import portal
|
||||
from evennia.server import serversession, session
|
||||
from evennia.utils import create
|
||||
|
||||
from twisted.internet.base import DelayedCall
|
||||
DelayedCall.debug = True
|
||||
|
||||
class _TestAMP(TwistedTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(_TestAMP, self).setUp()
|
||||
with patch("evennia.server.initial_setup.get_god_account") as mockgod:
|
||||
self.account = create.account("TestAMPAccount", "test@test.com", "testpassword")
|
||||
mockgod.return_value = self.account
|
||||
self.server = server.Evennia(MagicMock())
|
||||
self.server.sessions.data_in = MagicMock()
|
||||
self.server.sessions.data_out = MagicMock()
|
||||
self.amp_client_factory = amp_client.AMPClientFactory(self.server)
|
||||
self.amp_client = self.amp_client_factory.buildProtocol("127.0.0.1")
|
||||
self.session = MagicMock() # serversession.ServerSession()
|
||||
self.session.sessid = 1
|
||||
self.server.sessions[1] = self.session
|
||||
|
||||
self.portal = portal.Portal(MagicMock())
|
||||
self.portalsession = session.Session()
|
||||
self.portalsession.sessid = 1
|
||||
self.portal.sessions[1] = self.portalsession
|
||||
self.portal.sessions.data_in = MagicMock()
|
||||
self.portal.sessions.data_out = MagicMock()
|
||||
self.amp_server_factory = amp_server.AMPServerFactory(self.portal)
|
||||
self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1")
|
||||
|
||||
def tearDown(self):
|
||||
self.account.delete()
|
||||
super(_TestAMP, self).tearDown()
|
||||
|
||||
def _connect_client(self, mocktransport):
|
||||
"Setup client to send data for testing"
|
||||
mocktransport.write = MagicMock()
|
||||
self.amp_client.makeConnection(mocktransport)
|
||||
mocktransport.write.reset_mock()
|
||||
|
||||
def _connect_server(self, mocktransport):
|
||||
"Setup server to send data for testing"
|
||||
mocktransport.write = MagicMock()
|
||||
self.amp_server.makeConnection(mocktransport)
|
||||
mocktransport.write.reset_mock()
|
||||
|
||||
def _catch_wire_read(self, mocktransport):
|
||||
"Parse what was supposed to be sent over the wire"
|
||||
arg_list = mocktransport.write.call_args_list
|
||||
|
||||
all_sent = []
|
||||
for i, cll in enumerate(arg_list):
|
||||
args, kwargs = cll
|
||||
raw_inp = args[0]
|
||||
all_sent.append(raw_inp)
|
||||
|
||||
return all_sent
|
||||
|
||||
|
||||
@patch("evennia.server.server.LoopingCall", MagicMock())
|
||||
@patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport")
|
||||
class TestAMPClientSend(_TestAMP):
|
||||
"""Test amp client sending data"""
|
||||
|
||||
def test_msgserver2portal(self, mocktransport):
|
||||
self._connect_client(mocktransport)
|
||||
self.amp_client.send_MsgServer2Portal(self.session, text={"foo": "bar"})
|
||||
wire_data = self._catch_wire_read(mocktransport)[0]
|
||||
|
||||
self._connect_server(mocktransport)
|
||||
self.amp_server.dataReceived(wire_data)
|
||||
self.portal.sessions.data_out.assert_called_with(self.portalsession, text={"foo": "bar"})
|
||||
|
||||
def test_adminserver2portal(self, mocktransport):
|
||||
self._connect_client(mocktransport)
|
||||
|
||||
self.amp_client.send_AdminServer2Portal(self.session,
|
||||
operation=amp.PSYNC,
|
||||
info_dict={}, spid=None)
|
||||
wire_data = self._catch_wire_read(mocktransport)[0]
|
||||
|
||||
self._connect_server(mocktransport)
|
||||
self.amp_server.data_in = MagicMock()
|
||||
self.amp_server.dataReceived(wire_data)
|
||||
self.amp_server.data_in.assert_called()
|
||||
|
||||
|
||||
@patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport")
|
||||
class TestAMPClientRecv(_TestAMP):
|
||||
"""Test amp client sending data"""
|
||||
|
||||
def test_msgportal2server(self, mocktransport):
|
||||
self._connect_server(mocktransport)
|
||||
self.amp_server.send_MsgPortal2Server(self.session, text={"foo": "bar"})
|
||||
wire_data = self._catch_wire_read(mocktransport)[0]
|
||||
|
||||
self._connect_client(mocktransport)
|
||||
self.amp_client.dataReceived(wire_data)
|
||||
self.server.sessions.data_in.assert_called_with(self.session, text={"foo": "bar"})
|
||||
|
||||
def test_adminportal2server(self, mocktransport):
|
||||
self._connect_server(mocktransport)
|
||||
|
||||
self.amp_server.send_AdminPortal2Server(self.session,
|
||||
operation=amp.PDISCONNALL)
|
||||
wire_data = self._catch_wire_read(mocktransport)[0]
|
||||
|
||||
self._connect_client(mocktransport)
|
||||
self.server.sessions.portal_disconnect_all = MagicMock()
|
||||
self.amp_client.dataReceived(wire_data)
|
||||
self.server.sessions.portal_disconnect_all.assert_called()
|
||||
197
evennia/server/tests/test_launcher.py
Normal file
197
evennia/server/tests/test_launcher.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
"""
|
||||
Test the evennia launcher.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
from anything import Something
|
||||
from mock import patch, MagicMock, create_autospec
|
||||
from twisted.internet import reactor
|
||||
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||
from evennia.server import evennia_launcher
|
||||
from evennia.server.portal import amp
|
||||
|
||||
from twisted.internet.base import DelayedCall
|
||||
DelayedCall.debug = True
|
||||
|
||||
@patch("evennia.server.evennia_launcher.Popen", new=MagicMock())
|
||||
class TestLauncher(TwistedTestCase):
|
||||
|
||||
def test_is_windows(self):
|
||||
self.assertEqual(evennia_launcher._is_windows(), os.name == 'nt')
|
||||
|
||||
def test_file_compact(self):
|
||||
self.assertEqual(evennia_launcher._file_names_compact(
|
||||
"foo/bar/test1", "foo/bar/test2"),
|
||||
"foo/bar/test1 and test2")
|
||||
|
||||
self.assertEqual(evennia_launcher._file_names_compact(
|
||||
"foo/test1", "foo/bar/test2"),
|
||||
"foo/test1 and foo/bar/test2")
|
||||
|
||||
@patch("evennia.server.evennia_launcher.print")
|
||||
def test_print_info(self, mockprint):
|
||||
portal_dict = {
|
||||
"servername": "testserver",
|
||||
"version": "1",
|
||||
"telnet": 1234,
|
||||
"telnet_ssl": [1234, 2345],
|
||||
"ssh": 1234,
|
||||
"webserver_proxy": 1234,
|
||||
"webclient": 1234,
|
||||
"webserver_internal": 1234,
|
||||
"amp": 1234
|
||||
}
|
||||
server_dict = {
|
||||
"servername": "testserver",
|
||||
"version": "1",
|
||||
"webserver": [1234, 1234],
|
||||
"amp": 1234,
|
||||
"irc_rss": "irc.test",
|
||||
"info": "testing mode",
|
||||
"errors": ""
|
||||
}
|
||||
|
||||
evennia_launcher._print_info(portal_dict, server_dict)
|
||||
mockprint.assert_called()
|
||||
|
||||
def test_parse_status(self):
|
||||
response = {"status": pickle.dumps(("teststring",))}
|
||||
result = evennia_launcher._parse_status(response)
|
||||
self.assertEqual(result, ("teststring",))
|
||||
|
||||
@patch("evennia.server.evennia_launcher.os.name", new="posix")
|
||||
def test_get_twisted_cmdline(self):
|
||||
pcmd, scmd = evennia_launcher._get_twistd_cmdline(False, False)
|
||||
self.assertTrue("portal.py" in pcmd[1])
|
||||
self.assertTrue("--pidfile" in pcmd[2])
|
||||
self.assertTrue("server.py" in scmd[1])
|
||||
self.assertTrue("--pidfile" in scmd[2])
|
||||
|
||||
pcmd, scmd = evennia_launcher._get_twistd_cmdline(True, True)
|
||||
self.assertTrue("portal.py" in pcmd[1])
|
||||
self.assertTrue("--pidfile" in pcmd[2])
|
||||
self.assertTrue("--profiler=cprofile" in pcmd[4], "actual: {}".format(pcmd))
|
||||
self.assertTrue("--profile=" in pcmd[5])
|
||||
self.assertTrue("server.py" in scmd[1])
|
||||
self.assertTrue("--pidfile" in scmd[2])
|
||||
self.assertTrue("--pidfile" in scmd[2])
|
||||
self.assertTrue("--profiler=cprofile" in scmd[4], "actual: {}".format(scmd))
|
||||
self.assertTrue("--profile=" in scmd[5])
|
||||
|
||||
@patch("evennia.server.evennia_launcher.os.name", new="nt")
|
||||
def test_get_twisted_cmdline_nt(self):
|
||||
pcmd, scmd = evennia_launcher._get_twistd_cmdline(False, False)
|
||||
self.assertTrue(len(pcmd) == 2, "actual: {}".format(pcmd))
|
||||
self.assertTrue(len(scmd) == 2, "actual: {}".format(scmd))
|
||||
|
||||
@patch("evennia.server.evennia_launcher.reactor.stop")
|
||||
def test_reactor_stop(self, mockstop):
|
||||
evennia_launcher._reactor_stop()
|
||||
mockstop.assert_called()
|
||||
|
||||
def _catch_wire_read(self, mocktransport):
|
||||
"Parse what was supposed to be sent over the wire"
|
||||
arg_list = mocktransport.write.call_args_list
|
||||
|
||||
all_sent = []
|
||||
for i, cll in enumerate(arg_list):
|
||||
args, kwargs = cll
|
||||
raw_inp = args[0]
|
||||
all_sent.append(raw_inp)
|
||||
|
||||
return all_sent
|
||||
|
||||
# @patch("evennia.server.portal.amp.amp.BinaryBoxProtocol.transport")
|
||||
# def test_send_instruction_pstatus(self, mocktransport):
|
||||
|
||||
# deferred = evennia_launcher.send_instruction(
|
||||
# evennia_launcher.PSTATUS,
|
||||
# (),
|
||||
# callback=MagicMock(),
|
||||
# errback=MagicMock())
|
||||
|
||||
# on_wire = self._catch_wire_read(mocktransport)
|
||||
# self.assertEqual(on_wire, "")
|
||||
|
||||
# return deferred
|
||||
|
||||
def _msend_status_ok(operation, arguments, callback=None, errback=None):
|
||||
callback({"status": pickle.dumps((True, True, 2, 24, "info1", "info2"))})
|
||||
|
||||
def _msend_status_err(operation, arguments, callback=None, errback=None):
|
||||
errback({"status": pickle.dumps((False, False, 3, 25, "info3", "info4"))})
|
||||
|
||||
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok)
|
||||
@patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True)
|
||||
@patch("evennia.server.evennia_launcher.get_pid", MagicMock(return_value=100))
|
||||
@patch("evennia.server.evennia_launcher.print")
|
||||
def test_query_status_run(self, mprint):
|
||||
evennia_launcher.query_status()
|
||||
mprint.assert_called_with('Portal: RUNNING (pid 100)\nServer: RUNNING (pid 100)')
|
||||
|
||||
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_err)
|
||||
@patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True)
|
||||
@patch("evennia.server.evennia_launcher.print")
|
||||
def test_query_status_not_run(self, mprint):
|
||||
evennia_launcher.query_status()
|
||||
mprint.assert_called_with('Portal: NOT RUNNING\nServer: NOT RUNNING')
|
||||
|
||||
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok)
|
||||
@patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True)
|
||||
def test_query_status_callback(self):
|
||||
mprint = MagicMock()
|
||||
|
||||
def testcall(response):
|
||||
resp = pickle.loads(response['status'])
|
||||
mprint(resp)
|
||||
|
||||
evennia_launcher.query_status(callback=testcall)
|
||||
mprint.assert_called_with((True, True, 2, 24, "info1", "info2"))
|
||||
|
||||
@patch("evennia.server.evennia_launcher.AMP_CONNECTION")
|
||||
@patch("evennia.server.evennia_launcher.print")
|
||||
def test_wait_for_status_reply(self, mprint, aconn):
|
||||
aconn.wait_for_status = MagicMock()
|
||||
|
||||
def test():
|
||||
pass
|
||||
|
||||
evennia_launcher.wait_for_status_reply(test)
|
||||
aconn.wait_for_status.assert_called_with(test)
|
||||
|
||||
@patch("evennia.server.evennia_launcher.AMP_CONNECTION", None)
|
||||
@patch("evennia.server.evennia_launcher.print")
|
||||
def test_wait_for_status_reply_fail(self, mprint):
|
||||
evennia_launcher.wait_for_status_reply(None)
|
||||
mprint.assert_called_with("No Evennia connection established.")
|
||||
|
||||
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok)
|
||||
@patch("evennia.server.evennia_launcher.reactor.callLater")
|
||||
def test_wait_for_status(self, mcalllater):
|
||||
mcall = MagicMock()
|
||||
merr = MagicMock()
|
||||
evennia_launcher.wait_for_status(
|
||||
portal_running=True,
|
||||
server_running=True,
|
||||
callback=mcall,
|
||||
errback=merr)
|
||||
|
||||
mcall.assert_called_with(True, True)
|
||||
merr.assert_not_called()
|
||||
|
||||
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_err)
|
||||
@patch("evennia.server.evennia_launcher.reactor.callLater")
|
||||
def test_wait_for_status_fail(self, mcalllater):
|
||||
mcall = MagicMock()
|
||||
merr = MagicMock()
|
||||
evennia_launcher.wait_for_status(
|
||||
portal_running=True,
|
||||
server_running=True,
|
||||
callback=mcall,
|
||||
errback=merr)
|
||||
|
||||
mcall.assert_not_called()
|
||||
merr.assert_not_called()
|
||||
mcalllater.assert_called()
|
||||
242
evennia/server/tests/test_server.py
Normal file
242
evennia/server/tests/test_server.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
"""
|
||||
Test the main server component
|
||||
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from mock import MagicMock, patch, DEFAULT, call
|
||||
from django.test import override_settings
|
||||
from evennia.utils.test_resources import unload_module
|
||||
|
||||
|
||||
@patch("evennia.server.server.LoopingCall", new=MagicMock())
|
||||
class TestServer(TestCase):
|
||||
"""
|
||||
Test server module.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from evennia.server import server
|
||||
self.server = server
|
||||
|
||||
def test__server_maintenance_reset(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=0,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
mocks['connection'].close = MagicMock()
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=456)
|
||||
|
||||
# flush cache
|
||||
self.server._server_maintenance()
|
||||
mocks['ServerConfig'].objects.conf.assert_called_with('runtime', 456)
|
||||
|
||||
def test__server_maintenance_flush(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=600 - 1,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
mocks['connection'].close = MagicMock()
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
|
||||
|
||||
# flush cache
|
||||
self.server._server_maintenance()
|
||||
mocks['_FLUSH_CACHE'].assert_called_with(1000)
|
||||
|
||||
def test__server_maintenance_validate_scripts(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=3600 - 1,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
mocks['connection'].close = MagicMock()
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
|
||||
with patch("evennia.server.server.evennia.ScriptDB.objects.validate") as mock:
|
||||
self.server._server_maintenance()
|
||||
mocks['_FLUSH_CACHE'].assert_called_with(1000)
|
||||
mock.assert_called()
|
||||
|
||||
def test__server_maintenance_channel_handler_update(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=3700 - 1,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
mocks['connection'].close = MagicMock()
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
|
||||
with patch("evennia.server.server.evennia.CHANNEL_HANDLER.update") as mock:
|
||||
self.server._server_maintenance()
|
||||
mock.assert_called()
|
||||
|
||||
def test__server_maintenance_close_connection(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=(3600 * 7) - 1,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
mocks['connection'].close = MagicMock()
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
|
||||
self.server._server_maintenance()
|
||||
mocks['connection'].close.assert_called()
|
||||
|
||||
def test__server_maintenance_idle_time(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
LoopingCall=DEFAULT,
|
||||
Evennia=DEFAULT,
|
||||
_FLUSH_CACHE=DEFAULT,
|
||||
connection=DEFAULT,
|
||||
_IDMAPPER_CACHE_MAXSIZE=1000,
|
||||
_MAINTENANCE_COUNT=(3600 * 7) - 1,
|
||||
SESSIONS=DEFAULT,
|
||||
_IDLE_TIMEOUT=10,
|
||||
time=DEFAULT,
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
sess1 = MagicMock()
|
||||
sess2 = MagicMock()
|
||||
sess3 = MagicMock()
|
||||
sess4 = MagicMock()
|
||||
sess1.cmd_last = 100 # should time out
|
||||
sess2.cmd_last = 999 # should not time out
|
||||
sess3.cmd_last = 100 # should not time (due to account)
|
||||
sess4.cmd_last = 100 # should time out (due to access)
|
||||
sess1.account = None
|
||||
sess2.account = None
|
||||
sess3.account = MagicMock()
|
||||
sess3.account = MagicMock()
|
||||
sess4.account.access = MagicMock(return_value=False)
|
||||
|
||||
mocks['time'].time = MagicMock(return_value=1000)
|
||||
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
|
||||
mocks["SESSIONS"].values = MagicMock(return_value=[sess1, sess2, sess3, sess4])
|
||||
mocks["SESSIONS"].disconnect = MagicMock()
|
||||
|
||||
self.server._server_maintenance()
|
||||
reason = "idle timeout exceeded"
|
||||
calls = [call(sess1, reason=reason), call(sess4, reason=reason)]
|
||||
mocks["SESSIONS"].disconnect.assert_has_calls(calls, any_order=True)
|
||||
|
||||
def test_evennia_start(self):
|
||||
with patch.multiple("evennia.server.server",
|
||||
time=DEFAULT,
|
||||
service=DEFAULT) as mocks:
|
||||
|
||||
mocks['time'].time = MagicMock(return_value=1000)
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
self.assertEqual(evennia.start_time, 1000)
|
||||
|
||||
@patch("evennia.objects.models.ObjectDB")
|
||||
@patch("evennia.server.server.AccountDB")
|
||||
@patch("evennia.server.server.ScriptDB")
|
||||
@patch("evennia.comms.models.ChannelDB")
|
||||
def test_update_defaults(self, mockchan, mockscript, mockacct, mockobj):
|
||||
with patch.multiple("evennia.server.server",
|
||||
ServerConfig=DEFAULT) as mocks:
|
||||
|
||||
mockchan.objects.filter = MagicMock()
|
||||
mockscript.objects.filter = MagicMock()
|
||||
mockacct.objects.filter = MagicMock()
|
||||
mockobj.objects.filter = MagicMock()
|
||||
|
||||
# fake mismatches
|
||||
settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT",
|
||||
"BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS",
|
||||
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS",
|
||||
"BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS",
|
||||
"BASE_CHANNEL_TYPECLASS")
|
||||
fakes = {name: "Dummy.path" for name in settings_names}
|
||||
|
||||
def _mock_conf(key, *args):
|
||||
return fakes[key]
|
||||
|
||||
mocks['ServerConfig'].objects.conf = _mock_conf
|
||||
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
evennia.update_defaults()
|
||||
|
||||
mockchan.objects.filter.assert_called()
|
||||
mockscript.objects.filter.assert_called()
|
||||
mockacct.objects.filter.assert_called()
|
||||
mockobj.objects.filter.assert_called()
|
||||
|
||||
def test_initial_setup(self):
|
||||
from evennia.utils.create import create_account
|
||||
|
||||
acct = create_account("TestSuperuser", "test@test.com", "testpassword",
|
||||
is_superuser=True)
|
||||
|
||||
with patch.multiple("evennia.server.initial_setup",
|
||||
reset_server=DEFAULT,
|
||||
AccountDB=DEFAULT) as mocks:
|
||||
mocks['AccountDB'].objects.get = MagicMock(return_value=acct)
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
evennia.run_initial_setup()
|
||||
acct.delete()
|
||||
|
||||
def test_initial_setup_retry(self):
|
||||
from evennia.utils.create import create_account
|
||||
|
||||
acct = create_account("TestSuperuser2", "test@test.com", "testpassword",
|
||||
is_superuser=True)
|
||||
|
||||
with patch.multiple("evennia.server.initial_setup",
|
||||
ServerConfig=DEFAULT,
|
||||
reset_server=DEFAULT,
|
||||
AccountDB=DEFAULT) as mocks:
|
||||
mocks['AccountDB'].objects.get = MagicMock(return_value=acct)
|
||||
# a last_initial_setup_step > 0
|
||||
mocks['ServerConfig'].objects.conf = MagicMock(return_value=4)
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
evennia.run_initial_setup()
|
||||
acct.delete()
|
||||
|
||||
@override_settings(DEFAULT_HOME="#1")
|
||||
def test_run_init_hooks(self):
|
||||
from evennia.utils import create
|
||||
obj1 = create.object(key="HookTestObj1")
|
||||
obj2 = create.object(key="HookTestObj2")
|
||||
acct1 = create.account("HookAcct1", "hooktest1@test.com", "testpasswd")
|
||||
acct2 = create.account("HookAcct2", "hooktest2@test.com", "testpasswd")
|
||||
|
||||
with patch("evennia.objects.models.ObjectDB") as mockobj:
|
||||
with patch("evennia.server.server.AccountDB") as mockacct:
|
||||
|
||||
mockacct.get_all_cached_instances = MagicMock(return_value=[acct1, acct2])
|
||||
mockobj.get_all_cached_instances = MagicMock(return_value=[obj1, obj2])
|
||||
mockobj.objects.clear_all_sessids = MagicMock()
|
||||
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
evennia.run_init_hooks('reload')
|
||||
evennia.run_init_hooks('reset')
|
||||
evennia.run_init_hooks('shutdown')
|
||||
|
||||
mockacct.get_all_cached_instances.assert_called()
|
||||
mockobj.get_all_cached_instances.assert_called()
|
||||
mockobj.objects.clear_all_sessids.assert_called_with()
|
||||
obj1.delete()
|
||||
obj2.delete()
|
||||
acct1.delete()
|
||||
acct2.delete()
|
||||
|
||||
@patch('evennia.server.server.INFO_DICT', {"test": "foo"})
|
||||
def test_get_info_dict(self):
|
||||
evennia = self.server.Evennia(MagicMock())
|
||||
self.assertEqual(evennia.get_info_dict(), {"test": "foo"})
|
||||
|
|
@ -383,7 +383,9 @@ class AttributeHandler(object):
|
|||
|
||||
"""
|
||||
ret = []
|
||||
category = category.strip().lower() if category is not None else None
|
||||
for keystr in make_iter(key):
|
||||
keystr = key.strip().lower()
|
||||
ret.extend(bool(attr) for attr in self._getcache(keystr, category))
|
||||
return ret[0] if len(ret) == 1 else ret
|
||||
|
||||
|
|
@ -605,7 +607,8 @@ class AttributeHandler(object):
|
|||
Remove attribute or a list of attributes from object.
|
||||
|
||||
Args:
|
||||
key (str): An Attribute key to remove.
|
||||
key (str or list): An Attribute key to remove or a list of keys. If
|
||||
multiple keys, they must all be of the same `category`.
|
||||
raise_exception (bool, optional): If set, not finding the
|
||||
Attribute to delete will raise an exception instead of
|
||||
just quietly failing.
|
||||
|
|
@ -623,7 +626,11 @@ class AttributeHandler(object):
|
|||
was found matching `key`.
|
||||
|
||||
"""
|
||||
category = category.strip().lower() if category is not None else None
|
||||
|
||||
for keystr in make_iter(key):
|
||||
keystr = keystr.lower()
|
||||
|
||||
attr_objs = self._getcache(keystr, category)
|
||||
for attr_obj in attr_objs:
|
||||
if not (
|
||||
|
|
@ -634,10 +641,11 @@ class AttributeHandler(object):
|
|||
try:
|
||||
attr_obj.delete()
|
||||
except AssertionError:
|
||||
print("Assertionerror for attr.delete()")
|
||||
# this happens if the attr was already deleted
|
||||
pass
|
||||
finally:
|
||||
self._delcache(key, category)
|
||||
self._delcache(keystr, category)
|
||||
if not attr_objs and raise_exception:
|
||||
raise AttributeError
|
||||
|
||||
|
|
@ -655,6 +663,8 @@ class AttributeHandler(object):
|
|||
type `attredit` on the Attribute in question.
|
||||
|
||||
"""
|
||||
category = category.strip().lower() if category is not None else None
|
||||
|
||||
if not self._cache_complete:
|
||||
self._fullcache()
|
||||
if accessing_obj:
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ def create_account(key, email, password,
|
|||
typeclass=None,
|
||||
is_superuser=False,
|
||||
locks=None, permissions=None,
|
||||
tags=None, attributes=None,
|
||||
report_to=None):
|
||||
"""
|
||||
This creates a new account.
|
||||
|
|
@ -414,15 +415,20 @@ def create_account(key, email, password,
|
|||
password (str): Password in cleartext.
|
||||
|
||||
Kwargs:
|
||||
typeclass (str): The typeclass to use for the account.
|
||||
is_superuser (bool): Wether or not this account is to be a superuser
|
||||
locks (str): Lockstring.
|
||||
permission (list): List of permission strings.
|
||||
tags (list): List of Tags on form `(key, category[, data])`
|
||||
attributes (list): List of Attributes on form
|
||||
`(key, value [, category, [,lockstring [, default_pass]]])`
|
||||
report_to (Object): An object with a msg() method to report
|
||||
errors to. If not given, errors will be logged.
|
||||
|
||||
Raises:
|
||||
ValueError: If `key` already exists in database.
|
||||
|
||||
|
||||
Notes:
|
||||
Usually only the server admin should need to be superuser, all
|
||||
other access levels can be handled with more fine-grained
|
||||
|
|
@ -437,6 +443,8 @@ def create_account(key, email, password,
|
|||
typeclass = typeclass if typeclass else settings.BASE_ACCOUNT_TYPECLASS
|
||||
locks = make_iter(locks) if locks is not None else None
|
||||
permissions = make_iter(permissions) if permissions is not None else None
|
||||
tags = make_iter(tags) if tags is not None else None
|
||||
attributes = make_iter(attributes) if attributes is not None else None
|
||||
|
||||
if isinstance(typeclass, str):
|
||||
# a path is given. Load the actual typeclass.
|
||||
|
|
@ -462,7 +470,8 @@ def create_account(key, email, password,
|
|||
is_staff=is_superuser, is_superuser=is_superuser,
|
||||
last_login=now, date_joined=now)
|
||||
new_account.set_password(password)
|
||||
new_account._createdict = dict(locks=locks, permissions=permissions, report_to=report_to)
|
||||
new_account._createdict = dict(locks=locks, permissions=permissions, report_to=report_to,
|
||||
tags=tags, attributes=attributes)
|
||||
# saving will trigger the signal that calls the
|
||||
# at_first_save hook on the typeclass, where the _createdict
|
||||
# can be used.
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ class CmdEditorGroup(CmdEditorBase):
|
|||
else:
|
||||
buf = linebuffer[:lstart] + editor._copy_buffer + linebuffer[lstart:]
|
||||
editor.update_buffer(buf)
|
||||
caller.msg("Copied buffer %s to %s." % (editor._copy_buffer, self.lstr))
|
||||
caller.msg("Pasted buffer %s to %s." % (editor._copy_buffer, self.lstr))
|
||||
elif cmd == ":i":
|
||||
# :i <l> <txt> - insert new line
|
||||
new_lines = self.args.split('\n')
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
"""
|
||||
Various helper resources for writing unittests.
|
||||
|
||||
"""
|
||||
import sys
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from mock import Mock
|
||||
|
|
@ -10,8 +15,38 @@ from evennia.utils import create
|
|||
from evennia.utils.idmapper.models import flush_cache
|
||||
|
||||
|
||||
SESSIONS.data_out = Mock()
|
||||
SESSIONS.disconnect = Mock()
|
||||
def unload_module(module):
|
||||
"""
|
||||
Reset import so one can mock global constants.
|
||||
|
||||
Args:
|
||||
module (module, object or str): The module will
|
||||
be removed so it will have to be imported again. If given
|
||||
an object, the module in which that object sits will be unloaded. A string
|
||||
should directly give the module pathname to unload.
|
||||
|
||||
Example:
|
||||
# (in a test method)
|
||||
unload_module(foo)
|
||||
with mock.patch("foo.GLOBALTHING", "mockval"):
|
||||
import foo
|
||||
... # test code using foo.GLOBALTHING, now set to 'mockval'
|
||||
|
||||
|
||||
This allows for mocking constants global to the module, since
|
||||
otherwise those would not be mocked (since a module is only
|
||||
loaded once).
|
||||
|
||||
"""
|
||||
if isinstance(module, basestring):
|
||||
modulename = module
|
||||
elif hasattr(module, "__module__"):
|
||||
modulename = module.__module__
|
||||
else:
|
||||
modulename = module.__name__
|
||||
|
||||
if modulename in sys.modules:
|
||||
del sys.modules[modulename]
|
||||
|
||||
|
||||
class EvenniaTest(TestCase):
|
||||
|
|
@ -29,6 +64,11 @@ class EvenniaTest(TestCase):
|
|||
"""
|
||||
Sets up testing environment
|
||||
"""
|
||||
self.backups = (SESSIONS.data_out, SESSIONS.disconnect,
|
||||
settings.DEFAULT_HOME, settings.PROTOTYPE_MODULES)
|
||||
SESSIONS.data_out = Mock()
|
||||
SESSIONS.disconnect = Mock()
|
||||
|
||||
self.account = create.create_account("TestAccount", email="test@test.com", password="testpassword", typeclass=self.account_typeclass)
|
||||
self.account2 = create.create_account("TestAccount2", email="test@test.com", password="testpassword", typeclass=self.account_typeclass)
|
||||
self.room1 = create.create_object(self.room_typeclass, key="Room", nohome=True)
|
||||
|
|
@ -62,6 +102,11 @@ class EvenniaTest(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
flush_cache()
|
||||
SESSIONS.data_out = self.backups[0]
|
||||
SESSIONS.disconnect = self.backups[1]
|
||||
settings.DEFAULT_HOME = self.backups[2]
|
||||
settings.PROTOTYPE_MODULES = self.backups[3]
|
||||
|
||||
del SESSIONS[self.session.sessid]
|
||||
self.account.delete()
|
||||
self.account2.delete()
|
||||
|
|
|
|||
93
evennia/utils/tests/test_eveditor.py
Normal file
93
evennia/utils/tests/test_eveditor.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Test eveditor
|
||||
|
||||
"""
|
||||
|
||||
from mock import MagicMock
|
||||
from django.test import TestCase
|
||||
from evennia.utils import eveditor
|
||||
from evennia.commands.default.tests import CommandTest
|
||||
|
||||
|
||||
class TestEvEditor(CommandTest):
|
||||
|
||||
def test_eveditor_view_cmd(self):
|
||||
eveditor.EvEditor(self.char1)
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":h",
|
||||
msg="<txt> - any non-command is appended to the end of the buffer.")
|
||||
# empty buffer
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":",
|
||||
msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)")
|
||||
# input a string
|
||||
self.call(eveditor.CmdLineInput(), "First test line", raw_string="First test line",
|
||||
msg="01First test line")
|
||||
self.call(eveditor.CmdLineInput(), "Second test line", raw_string="Second test line",
|
||||
msg="02Second test line")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(), 'First test line\nSecond test line')
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":", # view buffer
|
||||
msg="Line Editor []\n01First test line\n"
|
||||
"02Second test line\n[l:02 w:006 c:0032](:h for help)")
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring="::", # view buffer, no linenums
|
||||
msg="Line Editor []\nFirst test line\n"
|
||||
"Second test line\n[l:02 w:006 c:0032](:h for help)")
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":::", # add single : alone on row
|
||||
msg="Single ':' added to buffer.")
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":",
|
||||
msg="Line Editor []\n01First test line\n"
|
||||
"02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)")
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":dd", # delete line
|
||||
msg="Deleted line 3.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\nSecond test line')
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo
|
||||
msg="Undid one step.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\nSecond test line\n:')
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":uu", # redo
|
||||
msg="Redid one step.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\nSecond test line')
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo
|
||||
msg="Undid one step.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\nSecond test line\n:')
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":",
|
||||
msg="Line Editor []\n01First test line\n"
|
||||
"02Second test line\n03:\n[l:03 w:007 c:0034](:h for help)")
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "Second", cmdstring=":dw", # delete by word
|
||||
msg="Removed Second for lines 1-4.")
|
||||
self.call(eveditor.CmdEditorGroup(), "", cmdstring=":u", # undo
|
||||
msg="Undid one step.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2 Second", cmdstring=":dw", # delete by word/line
|
||||
msg="Removed Second for line 2.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\n test line\n:')
|
||||
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":p", # paste
|
||||
msg="Copy buffer is empty.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":y", # yank
|
||||
msg="Line 2, [' test line'] yanked.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2", cmdstring=":p", # paste
|
||||
msg="Pasted buffer [' test line'] to line 2.")
|
||||
self.assertEqual(self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\n test line\n test line\n:')
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "3", cmdstring=":x", # cut
|
||||
msg="Line 3, [' test line'] cut.")
|
||||
|
||||
self.call(eveditor.CmdEditorGroup(), "2 New Second line", cmdstring=":i", # insert
|
||||
msg="Inserted 1 new line(s) at line 2.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2 New Replaced Second line", # replace
|
||||
cmdstring=":r", msg="Replaced 1 line(s) at line 2.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2 Inserted-", # insert beginning line
|
||||
cmdstring=":I", msg="Inserted text at beginning of line 2.")
|
||||
self.call(eveditor.CmdEditorGroup(), "2 -End", # append end line
|
||||
cmdstring=":A", msg="Appended text to end of line 2.")
|
||||
|
||||
self.assertEqual(
|
||||
self.char1.ndb._eveditor.get_buffer(),
|
||||
'First test line\nInserted-New Replaced Second line-End\n test line\n:')
|
||||
|
|
@ -215,6 +215,7 @@ class TextToHTMLparser(object):
|
|||
text (str): Processed text.
|
||||
|
||||
"""
|
||||
return text
|
||||
return text.replace(r'\n', r'<br>')
|
||||
|
||||
def convert_urls(self, text):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}404 - Not Found{% endblock %}
|
||||
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Error 404</h1>
|
||||
<h4 class="card-subtitle">Page not found</h2>
|
||||
<hr />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}500 - Internal Server Error{% endblock %}
|
||||
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Error 500</h1>
|
||||
<h4 class="card-subtitle">Internal Server Error</h2>
|
||||
<hr />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}Admin{% endblock %}
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-block p-4">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Admin</h1>
|
||||
<p class="card-text">
|
||||
Welcome to the Evennia Admin Page. Here, you can edit many facets of accounts, characters, and other parts of the game.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block header_ext %}
|
||||
{% endblock %}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{{flatpage.title}}</h1>
|
||||
{{flatpage.content}}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}Home{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block header_ext %}
|
||||
<META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Login
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "website/base.html" %}
|
||||
|
||||
{% block titleblock %}To Be Implemented{% endblock %}
|
||||
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
<div class="col">
|
||||
<div class="card text-center">
|
||||
<h1 class="card-header">To Be Implemented...</h1>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
This feature has yet to be implemented, but rest assured that it will be!
|
||||
<br />
|
||||
<br />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue