Merge develop branch, resolve conflicts

This commit is contained in:
Griatch 2018-10-13 17:19:38 +02:00
commit 54e351f296
22 changed files with 258 additions and 175 deletions

View file

@ -1,13 +1,18 @@
# Changelog # Changelog
## Evennia 0.9 (2019) ## Evennia 0.9 (2018-2019)
Update to Python 3 Update to Python 3
- Use `python3 -m venv <myenvname>` - Use `python3 -m venv <myenvname>`
- Use `python3 -m pdb <script>` for debugging - Use `python3 -m pdb <script>` for debugging
### Misc ### Commands
- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation
question.
### Utils
- Swap argument order of `evennia.set_trace` to `set_trace(term_size=(140, 40), debugger='auto')` - Swap argument order of `evennia.set_trace` to `set_trace(term_size=(140, 40), debugger='auto')`
since the size is more likely to be changed on the command line. since the size is more likely to be changed on the command line.

View file

@ -7,7 +7,7 @@
# Usage: # Usage:
# cd to a folder where you want your game data to be (or where it already is). # cd to a folder where you want your game data to be (or where it already is).
# #
# docker run -it -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia # docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia
# #
# (If your OS does not support $PWD, replace it with the full path to your current # (If your OS does not support $PWD, replace it with the full path to your current
# folder). # folder).
@ -15,6 +15,14 @@
# You will end up in a shell where the `evennia` command is available. From here you # You will end up in a shell where the `evennia` command is available. From here you
# can install and run the game normally. Use Ctrl-D to exit the evennia docker container. # can install and run the game normally. Use Ctrl-D to exit the evennia docker container.
# #
# You can also start evennia directly by passing arguments to the folder:
#
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia evennia start -l
#
# This will start Evennia running as the core process of the container. Note that you *must* use -l
# or one of the foreground modes (like evennia ipstart) since otherwise the container will immediately
# die since no foreground process keeps it up.
#
# The evennia/evennia base image is found on DockerHub and can also be used # The evennia/evennia base image is found on DockerHub and can also be used
# as a base for creating your own custom containerized Evennia game. For more # as a base for creating your own custom containerized Evennia game. For more
# info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker . # info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker .
@ -58,7 +66,7 @@ WORKDIR /usr/src/game
ENV PS1 "evennia|docker \w $ " ENV PS1 "evennia|docker \w $ "
# startup a shell when we start the container # startup a shell when we start the container
ENTRYPOINT bash -c "source /usr/src/evennia/bin/unix/evennia-docker-start.sh" ENTRYPOINT ["/usr/src/evennia/bin/unix/evennia-docker-start.sh"]
# expose the telnet, webserver and websocket client ports # expose the telnet, webserver and websocket client ports
EXPOSE 4000 4001 4005 EXPOSE 4000 4001 4005

16
bin/unix/evennia-docker-start.sh Normal file → Executable file
View file

@ -1,10 +1,18 @@
#! /bin/bash #! /bin/sh
# called by the Dockerfile to start the server in docker mode # called by the Dockerfile to start the server in docker mode
# remove leftover .pid files (such as from when dropping the container) # remove leftover .pid files (such as from when dropping the container)
rm /usr/src/game/server/*.pid >& /dev/null || true rm /usr/src/game/server/*.pid >& /dev/null || true
# start evennia server; log to server.log but also output to stdout so it can PS1="evennia|docker \w $ "
# be viewed with docker-compose logs
exec 3>&1; evennia start -l cmd="$@"
output="Docker starting with argument '$cmd' ..."
if test -z $cmd; then
cmd="bash"
output="No argument given, starting shell ..."
fi
echo $output
exec 3>&1; $cmd

View file

@ -1000,7 +1000,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
if target and not is_iter(target): if target and not is_iter(target):
# single target - just show it # single target - just show it
return target.return_appearance(self) if hasattr(target, "return_appearance"):
return target.return_appearance(self)
else:
return "{} has no in-game appearance.".format(target)
else: else:
# list of targets - make list to disconnect from db # list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else [] characters = list(tar for tar in target if tar) if target else []

View file

@ -1,7 +1,8 @@
from mock import Mock from mock import Mock, MagicMock
from random import randint from random import randint
from unittest import TestCase from unittest import TestCase
from django.test import override_settings
from evennia.accounts.accounts import AccountSessionHandler from evennia.accounts.accounts import AccountSessionHandler
from evennia.accounts.accounts import DefaultAccount from evennia.accounts.accounts import DefaultAccount
from evennia.server.session import Session from evennia.server.session import Session
@ -14,9 +15,15 @@ class TestAccountSessionHandler(TestCase):
"Check AccountSessionHandler class" "Check AccountSessionHandler class"
def setUp(self): def setUp(self):
self.account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) self.account = create.create_account(
"TestAccount%s" % randint(0, 999999), email="test@test.com",
password="testpassword", typeclass=DefaultAccount)
self.handler = AccountSessionHandler(self.account) self.handler = AccountSessionHandler(self.account)
def tearDown(self):
if hasattr(self, 'account'):
self.account.delete()
def test_get(self): def test_get(self):
"Check get method" "Check get method"
self.assertEqual(self.handler.get(), []) self.assertEqual(self.handler.get(), [])
@ -24,24 +31,24 @@ class TestAccountSessionHandler(TestCase):
import evennia.server.sessionhandler import evennia.server.sessionhandler
s1 = Session() s1 = MagicMock()
s1.logged_in = True s1.logged_in = True
s1.uid = self.account.uid s1.uid = self.account.uid
evennia.server.sessionhandler.SESSIONS[s1.uid] = s1 evennia.server.sessionhandler.SESSIONS[s1.uid] = s1
s2 = Session() s2 = MagicMock()
s2.logged_in = True s2.logged_in = True
s2.uid = self.account.uid + 1 s2.uid = self.account.uid + 1
evennia.server.sessionhandler.SESSIONS[s2.uid] = s2 evennia.server.sessionhandler.SESSIONS[s2.uid] = s2
s3 = Session() s3 = MagicMock()
s3.logged_in = False s3.logged_in = False
s3.uid = self.account.uid + 2 s3.uid = self.account.uid + 2
evennia.server.sessionhandler.SESSIONS[s3.uid] = s3 evennia.server.sessionhandler.SESSIONS[s3.uid] = s3
self.assertEqual(self.handler.get(), [s1]) self.assertEqual([s.uid for s in self.handler.get()], [s1.uid])
self.assertEqual(self.handler.get(self.account.uid), [s1]) self.assertEqual([s.uid for s in [self.handler.get(self.account.uid)]], [s1.uid])
self.assertEqual(self.handler.get(self.account.uid + 1), []) self.assertEqual([s.uid for s in self.handler.get(self.account.uid + 1)], [])
def test_all(self): def test_all(self):
"Check all method" "Check all method"
@ -56,9 +63,14 @@ class TestDefaultAccount(TestCase):
"Check DefaultAccount class" "Check DefaultAccount class"
def setUp(self): def setUp(self):
self.s1 = Session() self.s1 = MagicMock()
self.s1.puppet = None self.s1.puppet = None
self.s1.sessid = 0 self.s1.sessid = 0
self.s1.data_outj
def tearDown(self):
if hasattr(self, "account"):
self.account.delete()
def test_password_validation(self): def test_password_validation(self):
"Check password validators deny bad passwords" "Check password validators deny bad passwords"
@ -71,7 +83,6 @@ class TestDefaultAccount(TestCase):
"Check validators allow sufficiently complex passwords" "Check validators allow sufficiently complex passwords"
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"): for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
self.assertTrue(self.account.validate_password(better, account=self.account)[0]) self.assertTrue(self.account.validate_password(better, account=self.account)[0])
self.account.delete()
def test_password_change(self): def test_password_change(self):
"Check password setting and validation is working as expected" "Check password setting and validation is working as expected"
@ -109,7 +120,9 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler import evennia.server.sessionhandler
account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) account = create.create_account(
"TestAccount%s" % randint(0, 999999), email="test@test.com",
password="testpassword", typeclass=DefaultAccount)
self.s1.uid = account.uid self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
@ -131,10 +144,7 @@ class TestDefaultAccount(TestCase):
self.s1.uid = account.uid self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
self.s1.puppet = None self.s1.data_out = MagicMock()
self.s1.logged_in = True
self.s1.data_out = Mock(return_value=None)
obj = Mock() obj = Mock()
obj.access = Mock(return_value=False) obj.access = Mock(return_value=False)
@ -143,6 +153,7 @@ class TestDefaultAccount(TestCase):
self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet")) self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet"))
self.assertIsNone(obj.at_post_puppet.call_args) self.assertIsNone(obj.at_post_puppet.call_args)
@override_settings(MULTISESSION_MODE=0)
def test_puppet_object_joining_other_session(self): def test_puppet_object_joining_other_session(self):
"Check puppet_object method called, joining other session" "Check puppet_object method called, joining other session"
@ -154,15 +165,16 @@ class TestDefaultAccount(TestCase):
self.s1.puppet = None self.s1.puppet = None
self.s1.logged_in = True self.s1.logged_in = True
self.s1.data_out = Mock(return_value=None) self.s1.data_out = MagicMock()
obj = Mock() obj = Mock()
obj.access = Mock(return_value=True) obj.access = Mock(return_value=True)
obj.account = account obj.account = account
obj.sessions.all = MagicMock(return_value=[self.s1])
account.puppet_object(self.s1, obj) account.puppet_object(self.s1, obj)
# works because django.conf.settings.MULTISESSION_MODE is not in (1, 3) # works because django.conf.settings.MULTISESSION_MODE is not in (1, 3)
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions.")) self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions.|n"))
self.assertTrue(obj.at_post_puppet.call_args[1] == {}) self.assertTrue(obj.at_post_puppet.call_args[1] == {})
def test_puppet_object_already_puppeted(self): def test_puppet_object_already_puppeted(self):
@ -171,6 +183,7 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler import evennia.server.sessionhandler
account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount) account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
self.account = account
self.s1.uid = account.uid self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1 evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1

View file

@ -24,7 +24,7 @@ import time
from codecs import lookup as codecs_lookup from codecs import lookup as codecs_lookup
from django.conf import settings from django.conf import settings
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia.utils import utils, create, search, evtable from evennia.utils import utils, create, logger, search, evtable
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -172,6 +172,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
new_character.db.desc = "This is a character." new_character.db.desc = "This is a character."
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character." self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
% (new_character.key, new_character.key)) % (new_character.key, new_character.key))
logger.log_sec('Character Created: %s (Caller: %s, IP: %s).' % (new_character, account, self.session.address))
class CmdCharDelete(COMMAND_DEFAULT_CLASS): class CmdCharDelete(COMMAND_DEFAULT_CLASS):
@ -215,6 +216,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
caller.db._playable_characters = [pc for pc in caller.db._playable_characters if pc != delobj] caller.db._playable_characters = [pc for pc in caller.db._playable_characters if pc != delobj]
delobj.delete() delobj.delete()
self.msg("Character '%s' was permanently deleted." % key) self.msg("Character '%s' was permanently deleted." % key)
logger.log_sec('Character Deleted: %s (Caller: %s, IP: %s).' % (key, account, self.session.address))
else: else:
self.msg("Deletion was aborted.") self.msg("Deletion was aborted.")
del caller.ndb._char_to_delete del caller.ndb._char_to_delete
@ -280,8 +282,10 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
try: try:
account.puppet_object(session, new_character) account.puppet_object(session, new_character)
account.db._last_puppet = new_character account.db._last_puppet = new_character
logger.log_sec('Puppet Success: (Caller: %s, Target: %s, IP: %s).' % (account, new_character, self.session.address))
except RuntimeError as exc: except RuntimeError as exc:
self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc)) self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc))
logger.log_sec('Puppet Failed: %s (Caller: %s, Target: %s, IP: %s).' % (exc, account, new_character, self.session.address))
# note that this is inheriting from MuxAccountLookCommand, # note that this is inheriting from MuxAccountLookCommand,
@ -642,6 +646,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
account.set_password(newpass) account.set_password(newpass)
account.save() account.save()
self.msg("Password changed.") self.msg("Password changed.")
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, account, self.session.address))
class CmdQuit(COMMAND_DEFAULT_CLASS): class CmdQuit(COMMAND_DEFAULT_CLASS):

View file

@ -9,14 +9,14 @@ import re
from django.conf import settings from django.conf import settings
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.utils import evtable, search, class_from_module from evennia.utils import evtable, logger, search, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
# limit members for API inclusion # limit members for API inclusion
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelAccount", __all__ = ("CmdBoot", "CmdBan", "CmdUnban",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
@ -96,6 +96,9 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
session.msg(feedback) session.msg(feedback)
session.account.disconnect_session_from_account(session) session.account.disconnect_session_from_account(session)
if pobj and boot_list:
logger.log_sec('Booted: %s (Reason: %s, Caller: %s, IP: %s).' % (pobj, reason, caller, self.session.address))
# regex matching IP addresses with wildcards, eg. 233.122.4.* # regex matching IP addresses with wildcards, eg. 233.122.4.*
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}") IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
@ -130,7 +133,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
reason to be able to later remember why the ban was put in place. reason to be able to later remember why the ban was put in place.
It is often preferable to ban an account from the server than to It is often preferable to ban an account from the server than to
delete an account with @delaccount. If banned by name, that account delete an account with @accounts/delete. If banned by name, that account
account can no longer be logged into. account can no longer be logged into.
IP (Internet Protocol) address banning allows blocking all access IP (Internet Protocol) address banning allows blocking all access
@ -203,6 +206,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
banlist.append(bantup) banlist.append(bantup)
ServerConfig.objects.conf('server_bans', banlist) ServerConfig.objects.conf('server_bans', banlist)
self.caller.msg("%s-Ban |w%s|n was added." % (typ, ban)) self.caller.msg("%s-Ban |w%s|n was added." % (typ, ban))
logger.log_sec('Banned %s: %s (Caller: %s, IP: %s).' % (typ, ban.strip(), self.caller, self.session.address))
class CmdUnban(COMMAND_DEFAULT_CLASS): class CmdUnban(COMMAND_DEFAULT_CLASS):
@ -246,79 +250,10 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
ban = banlist[num - 1] ban = banlist[num - 1]
del banlist[num - 1] del banlist[num - 1]
ServerConfig.objects.conf('server_bans', banlist) ServerConfig.objects.conf('server_bans', banlist)
value = " ".join([s for s in ban[:2]])
self.caller.msg("Cleared ban %s: %s" % self.caller.msg("Cleared ban %s: %s" %
(num, " ".join([s for s in ban[:2]]))) (num, value))
logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address))
class CmdDelAccount(COMMAND_DEFAULT_CLASS):
"""
delete an account from the server
Usage:
@delaccount[/switch] <name> [: reason]
Switch:
delobj - also delete the account's currently
assigned in-game object.
Completely deletes a user from the server database,
making their nick and e-mail again available.
"""
key = "@delaccount"
switch_options = ("delobj",)
locks = "cmd:perm(delaccount) or perm(Developer)"
help_category = "Admin"
def func(self):
"""Implements the command."""
caller = self.caller
args = self.args
if hasattr(caller, 'account'):
caller = caller.account
if not args:
self.msg("Usage: @delaccount <account/user name or #id> [: reason]")
return
reason = ""
if ':' in args:
args, reason = [arg.strip() for arg in args.split(':', 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg('Could not find an account by that name.')
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
# one single match
account = accounts.first()
if not account.access(caller, 'delete'):
string = "You don't have the permissions to delete that account."
self.msg(string)
return
uname = account.username
# boot the account then delete
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
account.delete()
self.msg("Account %s was successfully deleted." % uname)
class CmdEmit(COMMAND_DEFAULT_CLASS): class CmdEmit(COMMAND_DEFAULT_CLASS):
@ -428,9 +363,9 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
account = caller.search_account(self.lhs) account = caller.search_account(self.lhs)
if not account: if not account:
return return
newpass = self.rhs newpass = self.rhs
# Validate password # Validate password
validated, error = account.validate_password(newpass) validated, error = account.validate_password(newpass)
if not validated: if not validated:
@ -438,13 +373,14 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
string = "\n".join(errors) string = "\n".join(errors)
caller.msg(string) caller.msg(string)
return return
account.set_password(newpass) account.set_password(newpass)
account.save() account.save()
self.msg("%s - new password set to '%s'." % (account.name, newpass)) self.msg("%s - new password set to '%s'." % (account.name, newpass))
if account.character != caller: if account.character != caller:
account.msg("%s has changed your password to '%s'." % (caller.name, account.msg("%s has changed your password to '%s'." % (caller.name,
newpass)) newpass))
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, caller, self.session.address))
class CmdPerm(COMMAND_DEFAULT_CLASS): class CmdPerm(COMMAND_DEFAULT_CLASS):
@ -526,6 +462,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
else: else:
caller_result.append("\nPermission %s removed from %s (if they existed)." % (perm, obj.name)) caller_result.append("\nPermission %s removed from %s (if they existed)." % (perm, obj.name))
target_result.append("\n%s revokes the permission(s) %s from you." % (caller.name, perm)) target_result.append("\n%s revokes the permission(s) %s from you." % (caller.name, perm))
logger.log_sec('Permissions Deleted: %s, %s (Caller: %s, IP: %s).' % (perm, obj, caller, self.session.address))
else: else:
# add a new permission # add a new permission
permissions = obj.permissions.all() permissions = obj.permissions.all()
@ -547,6 +484,8 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring)) caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring))
target_result.append("\n%s gives you (%s, %s) the permission '%s'." target_result.append("\n%s gives you (%s, %s) the permission '%s'."
% (caller.name, obj.name, plystring, perm)) % (caller.name, obj.name, plystring, perm))
logger.log_sec('Permissions Added: %s, %s (Caller: %s, IP: %s).' % (obj, perm, caller, self.session.address))
caller.msg("".join(caller_result).strip()) caller.msg("".join(caller_result).strip())
if target_result: if target_result:
obj.msg("".join(target_result).strip()) obj.msg("".join(target_result).strip())

View file

@ -737,12 +737,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
confirm += ", ".join(["#{}".format(obj.id) for obj in objs]) confirm += ", ".join(["#{}".format(obj.id) for obj in objs])
confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]" confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]"
answer = "" answer = ""
while answer.strip().lower() not in ("y", "yes", "n", "no"): answer = yield(confirm)
answer = yield(confirm) answer = self.default_confirm if answer == '' else answer
answer = self.default_confirm if answer == '' else answer
if answer.strip().lower() in ("n", "no"): if answer.strip().lower() in ("n", "no"):
caller.msg("Cancelled: no object was destroyed.") caller.msg("Canceled: no object was destroyed.")
delete = False delete = False
if delete: if delete:
@ -2888,7 +2887,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
"use the 'exec' prototype key.") "use the 'exec' prototype key.")
return None return None
try: try:
protlib.validate_prototype(prototype) # we homogenize first, to be more lenient
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err: except RuntimeError as err:
self.caller.msg(str(err)) self.caller.msg(str(err))
return return

View file

@ -55,7 +55,6 @@ class AccountCmdSet(CmdSet):
self.add(system.CmdPy()) self.add(system.CmdPy())
# Admin commands # Admin commands
self.add(admin.CmdDelAccount())
self.add(admin.CmdNewPassword()) self.add(admin.CmdNewPassword())
# Comm commands # Comm commands

View file

@ -7,13 +7,15 @@ make sure to homogenize self.caller to always be the account object
for easy handling. for easy handling.
""" """
import hashlib
import time
from django.conf import settings from django.conf import settings
from evennia.comms.models import ChannelDB, Msg from evennia.comms.models import ChannelDB, Msg
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.accounts import bots from evennia.accounts import bots
from evennia.comms.channelhandler import CHANNELHANDLER from evennia.comms.channelhandler import CHANNELHANDLER
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.utils import create, utils, evtable from evennia.utils import create, logger, utils, evtable
from evennia.utils.utils import make_iter, class_from_module from evennia.utils.utils import make_iter, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -367,6 +369,7 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
channel.delete() channel.delete()
CHANNELHANDLER.update() CHANNELHANDLER.update()
self.msg("Channel '%s' was destroyed." % channel_key) self.msg("Channel '%s' was destroyed." % channel_key)
logger.log_sec('Channel Deleted: %s (Caller: %s, IP: %s).' % (channel_key, caller, self.session.address))
class CmdCBoot(COMMAND_DEFAULT_CLASS): class CmdCBoot(COMMAND_DEFAULT_CLASS):
@ -432,6 +435,8 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
# disconnect account # disconnect account
channel.disconnect(account) channel.disconnect(account)
CHANNELHANDLER.update() CHANNELHANDLER.update()
logger.log_sec('Channel Boot: %s (Channel: %s, Reason: %s, Caller: %s, IP: %s).' % (
account, channel, reason, self.caller, self.session.address))
class CmdCemit(COMMAND_DEFAULT_CLASS): class CmdCemit(COMMAND_DEFAULT_CLASS):
@ -917,8 +922,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
self.msg("Account '%s' already exists and is not a bot." % botname) self.msg("Account '%s' already exists and is not a bot." % botname)
return return
else: else:
password = hashlib.md5(str(time.time())).hexdigest()[:11]
try: try:
bot = create.create_account(botname, None, None, typeclass=botclass) bot = create.create_account(botname, None, password, typeclass=botclass)
except Exception as err: except Exception as err:
self.msg("|rError, could not create the bot:|n '%s'." % err) self.msg("|rError, could not create the bot:|n '%s'." % err)
return return

View file

@ -18,7 +18,7 @@ from evennia.server.sessionhandler import SESSIONS
from evennia.scripts.models import ScriptDB from evennia.scripts.models import ScriptDB
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.utils import logger, utils, gametime, create from evennia.utils import logger, utils, gametime, create, search
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable
from evennia.utils.utils import crop, class_from_module from evennia.utils.utils import crop, class_from_module
@ -460,17 +460,22 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
class CmdAccounts(COMMAND_DEFAULT_CLASS): class CmdAccounts(COMMAND_DEFAULT_CLASS):
""" """
list all registered accounts Manage registered accounts
Usage: Usage:
@accounts [nr] @accounts [nr]
@accounts/delete <name or #id> [: reason]
Lists statistics about the Accounts registered with the game. Switches:
delete - delete an account from the server
By default, lists statistics about the Accounts registered with the game.
It will list the <nr> amount of latest registered accounts It will list the <nr> amount of latest registered accounts
If not given, <nr> defaults to 10. If not given, <nr> defaults to 10.
""" """
key = "@accounts" key = "@accounts"
aliases = ["@listaccounts"] aliases = ["@account", "@listaccounts"]
switch_options = ("delete", )
locks = "cmd:perm(listaccounts) or perm(Admin)" locks = "cmd:perm(listaccounts) or perm(Admin)"
help_category = "System" help_category = "System"
@ -478,6 +483,56 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
"""List the accounts""" """List the accounts"""
caller = self.caller caller = self.caller
args = self.args
if "delete" in self.switches:
account = getattr(caller, "account")
if not account or not account.check_permstring("Developer"):
caller.msg("You are not allowed to delete accounts.")
return
if not args:
caller.msg("Usage: @accounts/delete <name or #id> [: reason]")
return
reason = ""
if ":" in args:
args, reason = [arg.strip() for arg in args.split(":", 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg("Could not find an account by that name.")
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
account = accounts.first()
if not account.access(caller, "delete"):
self.msg("You don't have the permissions to delete that account.")
return
username = account.username
# ask for confirmation
confirm = ("It is often better to block access to an account rather than to delete it. "
"|yAre you sure you want to permanently delete "
"account '|n{}|y'|n yes/[no]?".format(username))
answer = yield(confirm)
if answer.lower() not in ('y', 'yes'):
caller.msg("Canceled deletion.")
return
# Boot the account then delete it.
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
logger.log_sec("Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)." % (account, reason, caller, self.session.address))
account.delete()
self.msg("Account %s was successfully deleted." % username)
return
# No switches, default to displaying a list of accounts.
if self.args and self.args.isdigit(): if self.args and self.args.isdigit():
nlim = int(self.args) nlim = int(self.args)
else: else:

View file

@ -475,14 +475,14 @@ class CmdShiftRoot(Command):
root_pos["blue"] -= 1 root_pos["blue"] -= 1
self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.") self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs straight down - you can only move it left or right.")
elif color == "blue": elif color == "blue":
if direction == "left": if direction == "left":
root_pos[color] = max(-1, root_pos[color] - 1) root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the root with small blue flowers to the left.") self.caller.msg("You shift the root with small blue flowers to the left.")
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]: if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
root_pos["red"] += 1 root_pos["red"] += 1
self.caller.msg("The reddish root is to big to fit as well, so that one falls away to the left.") self.caller.msg("The reddish root is too big to fit as well, so that one falls away to the left.")
elif direction == "right": elif direction == "right":
root_pos[color] = min(1, root_pos[color] + 1) root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the root adorned with small blue flowers to the right.") self.caller.msg("You shove the root adorned with small blue flowers to the right.")
@ -490,7 +490,7 @@ class CmdShiftRoot(Command):
root_pos["red"] -= 1 root_pos["red"] -= 1
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.") self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs straight down - you can only move it left or right.")
# now the horizontal roots (yellow/green). They can be moved up/down # now the horizontal roots (yellow/green). They can be moved up/down
elif color == "yellow": elif color == "yellow":
@ -507,7 +507,7 @@ class CmdShiftRoot(Command):
root_pos["green"] -= 1 root_pos["green"] -= 1
self.caller.msg("The weedy green root is shifted upwards to make room.") self.caller.msg("The weedy green root is shifted upwards to make room.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs across the wall - you can only move it up or down.")
elif color == "green": elif color == "green":
if direction == "up": if direction == "up":
root_pos[color] = max(-1, root_pos[color] - 1) root_pos[color] = max(-1, root_pos[color] - 1)
@ -522,7 +522,7 @@ class CmdShiftRoot(Command):
root_pos["yellow"] -= 1 root_pos["yellow"] -= 1
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.") self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs across the wall - you can only move it up or down.")
# we have moved the root. Store new position # we have moved the root. Store new position
self.obj.db.root_pos = root_pos self.obj.db.root_pos = root_pos

View file

@ -747,9 +747,16 @@ class CmdLookDark(Command):
""" """
caller = self.caller caller = self.caller
if random.random() < 0.75: # count how many searches we've done
nr_searches = caller.ndb.dark_searches
if nr_searches is None:
nr_searches = 0
caller.ndb.dark_searches = nr_searches
if nr_searches < 4 and random.random() < 0.90:
# we don't find anything # we don't find anything
caller.msg(random.choice(DARK_MESSAGES)) caller.msg(random.choice(DARK_MESSAGES))
caller.ndb.dark_searches += 1
else: else:
# we could have found something! # we could have found something!
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)): if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
@ -791,7 +798,8 @@ class CmdDarkNoMatch(Command):
def func(self): def func(self):
"""Implements the command.""" """Implements the command."""
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.") self.caller.msg("Until you find some light, there's not much you can do. "
"Try feeling around, maybe you'll find something helpful!")
class DarkCmdSet(CmdSet): class DarkCmdSet(CmdSet):
@ -814,7 +822,9 @@ class DarkCmdSet(CmdSet):
self.add(CmdLookDark()) self.add(CmdLookDark())
self.add(CmdDarkHelp()) self.add(CmdDarkHelp())
self.add(CmdDarkNoMatch()) self.add(CmdDarkNoMatch())
self.add(default_cmds.CmdSay) self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdQuit())
self.add(default_cmds.CmdHome())
class DarkRoom(TutorialRoom): class DarkRoom(TutorialRoom):

View file

@ -6,6 +6,8 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
""" """
import re import re
import hashlib
import time
from ast import literal_eval from ast import literal_eval
from django.conf import settings from django.conf import settings
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
@ -13,7 +15,7 @@ from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script from evennia.utils.create import create_script
from evennia.utils.utils import ( from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
get_all_typeclasses, to_str, dbref, justify) get_all_typeclasses, to_str, dbref, justify, class_from_module)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger from evennia.utils import logger
from evennia.utils import inlinefuncs, dbserialize from evennia.utils import inlinefuncs, dbserialize
@ -47,8 +49,8 @@ class ValidationError(RuntimeError):
def homogenize_prototype(prototype, custom_keys=None): def homogenize_prototype(prototype, custom_keys=None):
""" """
Homogenize the more free-form prototype (where undefined keys are non-category attributes) Homogenize the more free-form prototype supported pre Evennia 0.7 into the stricter form.
into the stricter form using `attrs` required by the system.
Args: Args:
prototype (dict): Prototype. prototype (dict): Prototype.
@ -56,18 +58,45 @@ def homogenize_prototype(prototype, custom_keys=None):
the default reserved keys. the default reserved keys.
Returns: Returns:
homogenized (dict): Prototype where all non-identified keys grouped as attributes. homogenized (dict): Prototype where all non-identified keys grouped as attributes and other
homogenizations like adding missing prototype_keys and setting a default typeclass.
""" """
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ()) reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
attrs = list(prototype.get('attrs', [])) # break reference attrs = list(prototype.get('attrs', [])) # break reference
tags = make_iter(prototype.get('tags', []))
homogenized_tags = []
homogenized = {} homogenized = {}
for key, val in prototype.items(): for key, val in prototype.items():
if key in reserved: if key in reserved:
homogenized[key] = val if key == 'tags':
for tag in tags:
if not is_iter(tag):
homogenized_tags.append((tag, None, None))
else:
homogenized_tags.append(tag)
else:
homogenized[key] = val
else: else:
# unassigned keys -> attrs
attrs.append((key, val, None, '')) attrs.append((key, val, None, ''))
if attrs: if attrs:
homogenized['attrs'] = attrs homogenized['attrs'] = attrs
if homogenized_tags:
homogenized['tags'] = homogenized_tags
# add required missing parts that had defaults before
if "prototype_key" not in prototype:
# assign a random hash as key
homogenized["prototype_key"] = "prototype-{}".format(
hashlib.md5(str(time.time())).hexdigest()[:7])
if "typeclass" not in prototype and "prototype_parent" not in prototype:
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
return homogenized return homogenized
@ -76,9 +105,12 @@ def homogenize_prototype(prototype, custom_keys=None):
for mod in settings.PROTOTYPE_MODULES: for mod in settings.PROTOTYPE_MODULES:
# to remove a default prototype, override it with an empty dict. # to remove a default prototype, override it with an empty dict.
# internally we store as (key, desc, locks, tags, prototype_dict) # internally we store as (key, desc, locks, tags, prototype_dict)
prots = [(prototype_key.lower(), homogenize_prototype(prot)) prots = []
for prototype_key, prot in all_from_module(mod).items() for variable_name, prot in all_from_module(mod).items():
if prot and isinstance(prot, dict)] if isinstance(prot, dict):
if "prototype_key" not in prot:
prot['prototype_key'] = variable_name.lower()
prots.append((prot['prototype_key'], homogenize_prototype(prot)))
# assign module path to each prototype_key for easy reference # assign module path to each prototype_key for easy reference
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots}) _MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
# make sure the prototype contains all meta info # make sure the prototype contains all meta info
@ -432,11 +464,13 @@ def validate_prototype(prototype, protkey=None, protparents=None,
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks " _flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.".format(protkey)) "a typeclass or a prototype_parent.".format(protkey))
if (strict and typeclass and typeclass not if strict and typeclass:
in get_all_typeclasses("evennia.objects.models.ObjectDB")): try:
_flags['errors'].append( class_from_module(typeclass)
"Prototype {} is based on typeclass {}, which could not be imported!".format( except ImportError as err:
protkey, typeclass)) _flags['errors'].append(
"{}: Prototype {} is based on typeclass {}, which could not be imported!".format(
err, protkey, typeclass))
# recursively traverese prototype_parent chain # recursively traverese prototype_parent chain

View file

@ -259,11 +259,11 @@ def prototype_from_object(obj):
if aliases: if aliases:
prot['aliases'] = aliases prot['aliases'] = aliases
tags = [(tag.db_key, tag.db_category, tag.db_data) tags = [(tag.db_key, tag.db_category, tag.db_data)
for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag] for tag in obj.tags.all(return_objs=True)]
if tags: if tags:
prot['tags'] = tags prot['tags'] = tags
attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all())) attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all()))
for attr in obj.attributes.get(return_obj=True, return_list=True) if attr] for attr in obj.attributes.all()]
if attrs: if attrs:
prot['attrs'] = attrs prot['attrs'] = attrs
@ -659,6 +659,10 @@ def spawn(*prototypes, **kwargs):
# get available protparents # get available protparents
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
if not kwargs.get("only_validate"):
# homogenization to be more lenient about prototype format when entering the prototype manually
prototypes = [protlib.homogenize_prototype(prot) for prot in prototypes]
# overload module's protparents with specifically given protparents # overload module's protparents with specifically given protparents
# we allow prototype_key to be the key of the protparent dict, to allow for module-level # we allow prototype_key to be the key of the protparent dict, to allow for module-level
# prototype imports. We need to insert prototype_key in this case # prototype imports. We need to insert prototype_key in this case
@ -711,8 +715,8 @@ def spawn(*prototypes, **kwargs):
val = prot.pop("tags", []) val = prot.pop("tags", [])
tags = [] tags = []
for (tag, category, data) in tags: for (tag, category, data) in val:
tags.append((init_spawn_value(val, str), category, data)) tags.append((init_spawn_value(tag, str), category, data))
prototype_key = prototype.get('prototype_key', None) prototype_key = prototype.get('prototype_key', None)
if prototype_key: if prototype_key:
@ -730,7 +734,7 @@ def spawn(*prototypes, **kwargs):
val = make_iter(prot.pop("attrs", [])) val = make_iter(prot.pop("attrs", []))
attributes = [] attributes = []
for (attrname, value, category, locks) in val: for (attrname, value, category, locks) in val:
attributes.append((attrname, init_spawn_value(val), category, locks)) attributes.append((attrname, init_spawn_value(value), category, locks))
simple_attributes = [] simple_attributes = []
for key, value in ((key, value) for key, value in prot.items() for key, value in ((key, value) for key, value in prot.items()

View file

@ -134,6 +134,7 @@ class TestUtils(EvenniaTest):
'prototype_key': Something, 'prototype_key': Something,
'prototype_locks': 'spawn:all();edit:all()', 'prototype_locks': 'spawn:all();edit:all()',
'prototype_tags': [], 'prototype_tags': [],
'tags': [(u'footag', u'foocategory', None)],
'typeclass': 'evennia.objects.objects.DefaultObject'}) 'typeclass': 'evennia.objects.objects.DefaultObject'})
self.assertEqual(old_prot, self.assertEqual(old_prot,
@ -182,6 +183,7 @@ class TestUtils(EvenniaTest):
'typeclass': ('evennia.objects.objects.DefaultObject', 'typeclass': ('evennia.objects.objects.DefaultObject',
'evennia.objects.objects.DefaultObject', 'KEEP'), 'evennia.objects.objects.DefaultObject', 'KEEP'),
'aliases': {'foo': ('foo', None, 'REMOVE')}, 'aliases': {'foo': ('foo', None, 'REMOVE')},
'tags': {u'footag': ((u'footag', u'foocategory', None), None, 'REMOVE')},
'prototype_desc': ('Built from Obj', 'prototype_desc': ('Built from Obj',
'New version of prototype', 'UPDATE'), 'New version of prototype', 'UPDATE'),
'permissions': {"Builder": (None, 'Builder', 'ADD')} 'permissions': {"Builder": (None, 'Builder', 'ADD')}
@ -200,6 +202,7 @@ class TestUtils(EvenniaTest):
'prototype_key': 'UPDATE', 'prototype_key': 'UPDATE',
'prototype_locks': 'KEEP', 'prototype_locks': 'KEEP',
'prototype_tags': 'KEEP', 'prototype_tags': 'KEEP',
'tags': 'REMOVE',
'typeclass': 'KEEP'} 'typeclass': 'KEEP'}
) )
@ -384,8 +387,9 @@ class TestPrototypeStorage(EvenniaTest):
prot3 = protlib.create_prototype(**self.prot3) prot3 = protlib.create_prototype(**self.prot3)
# partial match # partial match
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3]) with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3]) self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))

View file

@ -407,7 +407,7 @@ class ServerSession(Session):
else: else:
self.data_out(**kwargs) self.data_out(**kwargs)
def execute_cmd(self, raw_string, **kwargs): def execute_cmd(self, raw_string, session=None, **kwargs):
""" """
Do something as this object. This method is normally never Do something as this object. This method is normally never
called directly, instead incoming command instructions are called directly, instead incoming command instructions are
@ -417,6 +417,9 @@ class ServerSession(Session):
Args: Args:
raw_string (string): Raw command input raw_string (string): Raw command input
session (Session): This is here to make API consistent with
Account/Object.execute_cmd. If given, data is passed to
that Session, otherwise use self.
Kwargs: Kwargs:
Other keyword arguments will be added to the found command Other keyword arguments will be added to the found command
object instace as variables before it executes. This is object instace as variables before it executes. This is
@ -426,7 +429,7 @@ class ServerSession(Session):
""" """
# inject instruction into input stream # inject instruction into input stream
kwargs["text"] = ((raw_string,), {}) kwargs["text"] = ((raw_string,), {})
self.sessionhandler.data_in(self, **kwargs) self.sessionhandler.data_in(session or self, **kwargs)
def __eq__(self, other): def __eq__(self, other):
"""Handle session comparisons""" """Handle session comparisons"""

View file

@ -13,6 +13,7 @@ always be sure of what you have changed and what is default behaviour.
""" """
from builtins import range from builtins import range
from django.urls import reverse_lazy
import os import os
import sys import sys
@ -697,9 +698,9 @@ ROOT_URLCONF = 'web.urls'
# Where users are redirected after logging in via contrib.auth.login. # Where users are redirected after logging in via contrib.auth.login.
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
# Where to redirect users when using the @login_required decorator. # Where to redirect users when using the @login_required decorator.
LOGIN_URL = '/accounts/login' LOGIN_URL = reverse_lazy('login')
# Where to redirect users who wish to logout. # Where to redirect users who wish to logout.
LOGOUT_URL = '/accounts/login' LOGOUT_URL = reverse_lazy('logout')
# URL that handles the media served from MEDIA_ROOT. # URL that handles the media served from MEDIA_ROOT.
# Example: "http://media.lawrence.com" # Example: "http://media.lawrence.com"
MEDIA_URL = '/media/' MEDIA_URL = '/media/'

View file

@ -668,7 +668,7 @@ class AttributeHandler(object):
def all(self, accessing_obj=None, default_access=True): def all(self, accessing_obj=None, default_access=True):
""" """
Return all Attribute objects on this object. Return all Attribute objects on this object, regardless of category.
Args: Args:
accessing_obj (object, optional): Check the `attrread` accessing_obj (object, optional): Check the `attrread`

View file

@ -348,13 +348,14 @@ class TagHandler(object):
self._catcache = {} self._catcache = {}
self._cache_complete = False self._cache_complete = False
def all(self, return_key_and_category=False): def all(self, return_key_and_category=False, return_objs=False):
""" """
Get all tags in this handler, regardless of category. Get all tags in this handler, regardless of category.
Args: Args:
return_key_and_category (bool, optional): Return a list of return_key_and_category (bool, optional): Return a list of
tuples `[(key, category), ...]`. tuples `[(key, category), ...]`.
return_objs (bool, optional): Return tag objects.
Returns: Returns:
tags (list): A list of tag keys `[tagkey, tagkey, ...]` or tags (list): A list of tag keys `[tagkey, tagkey, ...]` or
@ -368,6 +369,8 @@ class TagHandler(object):
if return_key_and_category: if return_key_and_category:
# return tuple (key, category) # return tuple (key, category)
return [(to_str(tag.db_key), to_str(tag.db_category)) for tag in tags] return [(to_str(tag.db_key), to_str(tag.db_category)) for tag in tags]
elif return_objs:
return tags
else: else:
return [to_str(tag.db_key) for tag in tags] return [to_str(tag.db_key) for tag in tags]

View file

@ -8,6 +8,7 @@ let defaultin_plugin = (function () {
// //
// handle the default <enter> key triggering onSend() // handle the default <enter> key triggering onSend()
var onKeydown = function (event) { var onKeydown = function (event) {
$("#inputfield").focus();
if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift
var inputfield = $("#inputfield"); var inputfield = $("#inputfield");
var outtext = inputfield.val(); var outtext = inputfield.val();

View file

@ -43,14 +43,6 @@ let history_plugin = (function () {
history_pos = 0; history_pos = 0;
} }
//
// Go to the last history line
var end = function () {
// move to the end of the history stack
history_pos = 0;
return history[history.length -1];
}
// //
// Add input to the scratch line // Add input to the scratch line
var scratch = function (input) { var scratch = function (input) {
@ -69,28 +61,17 @@ let history_plugin = (function () {
var history_entry = null; var history_entry = null;
var inputfield = $("#inputfield"); var inputfield = $("#inputfield");
if (inputfield[0].selectionStart == inputfield.val().length) { if (code === 38) { // Arrow up
// Only process up/down arrow if cursor is at the end of the line. history_entry = back();
if (code === 38) { // Arrow up }
history_entry = back(); else if (code === 40) { // Arrow down
} history_entry = fwd();
else if (code === 40) { // Arrow down
history_entry = fwd();
}
} }
if (history_entry !== null) { if (history_entry !== null) {
// Doing a history navigation; replace the text in the input. // Doing a history navigation; replace the text in the input.
inputfield.val(history_entry); inputfield.val(history_entry);
} }
else {
// Save the current contents of the input to the history scratch area.
setTimeout(function () {
// Need to wait until after the key-up to capture the value.
scratch(inputfield.val());
end();
}, 0);
}
return false; return false;
} }
@ -99,6 +80,7 @@ let history_plugin = (function () {
// Listen for onSend lines to add to history // Listen for onSend lines to add to history
var onSend = function (line) { var onSend = function (line) {
add(line); add(line);
return null; // we are not returning an altered input line
} }
// //