Merge branch 'master' of https://github.com/evennia/evennia into puzzles

This commit is contained in:
Henddher Pedroza 2018-10-23 20:07:34 -05:00
commit 5a8999920d
16 changed files with 239 additions and 29 deletions

View file

@ -2,6 +2,11 @@
## Evennia 0.8 (2018) ## Evennia 0.8 (2018)
### Requirements
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
- Add `inflect` dependency for automatic pluralization of object names.
### Server/Portal ### Server/Portal
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program) - Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
@ -85,7 +90,6 @@
### General ### General
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
- Start structuring the `CHANGELOG` to list features in more detail. - Start structuring the `CHANGELOG` to list features in more detail.
- Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch. - Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch.
- Inflection and grouping of multiple objects in default room (an box, three boxes) - Inflection and grouping of multiple objects in default room (an box, three boxes)

View file

@ -1001,7 +1001,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

@ -612,12 +612,12 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
self.edit_handler() self.edit_handler()
return return
if self.rhs: if '=' in self.args:
# We have an = # We have an =
obj = caller.search(self.lhs) obj = caller.search(self.lhs)
if not obj: if not obj:
return return
desc = self.rhs desc = self.rhs or ''
else: else:
obj = caller.location or self.msg("|rYou can't describe oblivion.|n") obj = caller.location or self.msg("|rYou can't describe oblivion.|n")
if not obj: if not obj:
@ -2856,7 +2856,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
key = "@spawn" key = "@spawn"
aliases = ["olc"] aliases = ["olc"]
switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update") switch_options = ("noloc", "search", "list", "show", "examine", "save", "delete", "menu", "olc", "update", "edit")
locks = "cmd:perm(spawn) or perm(Builder)" locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building" help_category = "Building"
@ -2907,12 +2907,13 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
caller = self.caller caller = self.caller
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches: if self.cmdstring == "olc" or 'menu' in self.switches \
or 'olc' in self.switches or 'edit' in self.switches:
# OLC menu mode # OLC menu mode
prototype = None prototype = None
if self.lhs: if self.lhs:
key = self.lhs key = self.lhs
prototype = spawner.search_prototype(key=key, return_meta=True) prototype = protlib.search_prototype(key=key)
if len(prototype) > 1: if len(prototype) > 1:
caller.msg("More than one match for {}:\n{}".format( caller.msg("More than one match for {}:\n{}".format(
key, "\n".join(proto.get('prototype_key', '') for proto in prototype))) key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
@ -2920,6 +2921,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
elif prototype: elif prototype:
# one match # one match
prototype = prototype[0] prototype = prototype[0]
else:
# no match
caller.msg("No prototype '{}' was found.".format(key))
return
olc_menus.start_olc(caller, session=self.session, prototype=prototype) olc_menus.start_olc(caller, session=self.session, prototype=prototype)
return return

View file

@ -315,6 +315,24 @@ class TestBuilding(CommandTest):
def test_desc(self): def test_desc(self):
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).") self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
def test_empty_desc(self):
"""
empty desc sets desc as ''
"""
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#5).")
assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d
assert self.room1.db.desc == r1d
def test_desc_default_to_room(self):
"""no rhs changes room's desc"""
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#1).")
assert self.obj2.db.desc == o2d
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
def test_wipe(self): def test_wipe(self):
confirm = building.CmdDestroy.confirm confirm = building.CmdDestroy.confirm
building.CmdDestroy.confirm = False building.CmdDestroy.confirm = False
@ -460,6 +478,61 @@ class TestBuilding(CommandTest):
# Test listing commands # Test listing commands
self.call(building.CmdSpawn(), "/list", "Key ") self.call(building.CmdSpawn(), "/list", "Key ")
# @spawn/edit (missing prototype)
# brings up olc menu
msg = self.call(
building.CmdSpawn(),
'/edit')
assert 'Prototype wizard' in msg
# @spawn/edit with valid prototype
# brings up olc menu loaded with prototype
msg = self.call(
building.CmdSpawn(),
'/edit testball')
assert 'Prototype wizard' in msg
assert hasattr(self.char1.ndb._menutree, "olc_prototype")
assert dict == type(self.char1.ndb._menutree.olc_prototype) \
and 'prototype_key' in self.char1.ndb._menutree.olc_prototype \
and 'key' in self.char1.ndb._menutree.olc_prototype \
and 'testball' == self.char1.ndb._menutree.olc_prototype['prototype_key'] \
and 'Ball' == self.char1.ndb._menutree.olc_prototype['key']
assert 'Ball' in msg and 'testball' in msg
# @spawn/edit with valid prototype (synomym)
msg = self.call(
building.CmdSpawn(),
'/edit BALL')
assert 'Prototype wizard' in msg
assert 'Ball' in msg and 'testball' in msg
# @spawn/edit with invalid prototype
msg = self.call(
building.CmdSpawn(),
'/edit NO_EXISTS',
"No prototype 'NO_EXISTS' was found.")
# @spawn/examine (missing prototype)
# lists all prototypes that exist
msg = self.call(
building.CmdSpawn(),
'/examine')
assert 'testball' in msg and 'testprot' in msg
# @spawn/examine with valid prototype
# prints the prototype
msg = self.call(
building.CmdSpawn(),
'/examine BALL')
assert 'Ball' in msg and 'testball' in msg
# @spawn/examine with invalid prototype
# shows error
self.call(
building.CmdSpawn(),
'/examine NO_EXISTS',
"No prototype 'NO_EXISTS' was found.")
class TestComms(CommandTest): class TestComms(CommandTest):

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

@ -562,6 +562,7 @@ def node_index(caller):
text = """ text = """
|c --- Prototype wizard --- |n |c --- Prototype wizard --- |n
%s
A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
@ -599,6 +600,17 @@ def node_index(caller):
{pfuncs} {pfuncs}
""".format(pfuncs=_format_protfuncs()) """.format(pfuncs=_format_protfuncs())
# If a prototype is being edited, show its key and
# prototype_key under the title
loaded_prototype = ''
if 'prototype_key' in prototype \
or 'key' in prototype:
loaded_prototype = ' --- Editing: |y{}({})|n --- '.format(
prototype.get('key', ''),
prototype.get('prototype_key', '')
)
text = text % (loaded_prototype)
text = (text, helptxt) text = (text, helptxt)
options = [] options = []

View file

@ -107,9 +107,10 @@ for mod in settings.PROTOTYPE_MODULES:
# internally we store as (key, desc, locks, tags, prototype_dict) # internally we store as (key, desc, locks, tags, prototype_dict)
prots = [] prots = []
for variable_name, prot in all_from_module(mod).items(): for variable_name, prot in all_from_module(mod).items():
if "prototype_key" not in prot: if isinstance(prot, dict):
prot['prototype_key'] = variable_name.lower() if "prototype_key" not in prot:
prots.append((prot['prototype_key'], homogenize_prototype(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

View file

@ -325,7 +325,7 @@ MENU = \
| 7) Kill Server only (send kill signal to process) | | 7) Kill Server only (send kill signal to process) |
| 8) Kill Portal + Server | | 8) Kill Portal + Server |
+--- Information -----------------------------------------------+ +--- Information -----------------------------------------------+
| 9) Tail log files (quickly see errors) | | 9) Tail log files (quickly see errors - Ctrl-C to exit) |
| 10) Status | | 10) Status |
| 11) Port info | | 11) Port info |
+--- Testing ---------------------------------------------------+ +--- Testing ---------------------------------------------------+

View file

@ -84,7 +84,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
from evennia.utils.utils import delay from evennia.utils.utils import delay
# timeout the handshakes in case the client doesn't reply at all # timeout the handshakes in case the client doesn't reply at all
delay(2, callback=self.handshake_done, timeout=True) self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True)
# TCP/IP keepalive watches for dead links # TCP/IP keepalive watches for dead links
self.transport.setTcpKeepAlive(1) self.transport.setTcpKeepAlive(1)

View file

@ -8,9 +8,24 @@ try:
except ImportError: except ImportError:
import unittest import unittest
from mock import Mock
import string import string
from evennia.server.portal import irc from evennia.server.portal import irc
from twisted.conch.telnet import IAC, WILL, DONT, SB, SE, NAWS, DO
from twisted.test import proto_helpers
from twisted.trial.unittest import TestCase as TwistedTestCase
from .telnet import TelnetServerFactory, TelnetProtocol
from .portal import PORTAL_SESSIONS
from .suppress_ga import SUPPRESS_GA
from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH
from .ttype import TTYPE, IS
from .mccp import MCCP
from .mssp import MSSP
from .mxp import MXP
from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR
class TestIRC(TestCase): class TestIRC(TestCase):
@ -73,3 +88,64 @@ class TestIRC(TestCase):
s = r'|wthis|Xis|gis|Ma|C|complex|*string' s = r'|wthis|Xis|gis|Ma|C|complex|*string'
self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s) self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)
class TestTelnet(TwistedTestCase):
def setUp(self):
super(TestTelnet, self).setUp()
factory = TelnetServerFactory()
factory.protocol = TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS
factory.sessionhandler.portal = Mock()
self.proto = factory.buildProtocol(("localhost", 0))
self.transport = proto_helpers.StringTransport()
self.addCleanup(factory.sessionhandler.disconnect_all)
def test_mudlet_ttype(self):
self.transport.client = ["localhost"]
self.transport.setTcpKeepAlive = Mock()
d = self.proto.makeConnection(self.transport)
# test suppress_ga
self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])
self.proto.dataReceived(IAC + DONT + SUPPRESS_GA)
self.assertFalse(self.proto.protocol_flags["NOGOAHEAD"])
self.assertEqual(self.proto.handshakes, 7)
# test naws
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'], {0: DEFAULT_WIDTH})
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'], {0: DEFAULT_HEIGHT})
self.proto.dataReceived(IAC + WILL + NAWS)
self.proto.dataReceived([IAC, SB, NAWS, '', 'x', '', 'd', IAC, SE])
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 120)
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 100)
self.assertEqual(self.proto.handshakes, 6)
# test ttype
self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])
self.assertFalse(self.proto.protocol_flags["TTYPE"])
self.assertTrue(self.proto.protocol_flags["ANSI"])
self.proto.dataReceived(IAC + WILL + TTYPE)
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MUDLET", IAC, SE])
self.assertTrue(self.proto.protocol_flags["XTERM256"])
self.assertEqual(self.proto.protocol_flags["CLIENTNAME"], "MUDLET")
self.proto.dataReceived([IAC, SB, TTYPE, IS, "XTERM", IAC, SE])
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MTTS 137", IAC, SE])
self.assertEqual(self.proto.handshakes, 5)
# test mccp
self.proto.dataReceived(IAC + DONT + MCCP)
self.assertFalse(self.proto.protocol_flags['MCCP'])
self.assertEqual(self.proto.handshakes, 4)
# test mssp
self.proto.dataReceived(IAC + DONT + MSSP)
self.assertEqual(self.proto.handshakes, 3)
# test oob
self.proto.dataReceived(IAC + DO + MSDP)
self.proto.dataReceived([IAC, SB, MSDP, MSDP_VAR, "LIST", MSDP_VAL, "COMMANDS", IAC, SE])
self.assertTrue(self.proto.protocol_flags['OOB'])
self.assertEqual(self.proto.handshakes, 2)
# test mxp
self.proto.dataReceived(IAC + DONT + MXP)
self.assertFalse(self.proto.protocol_flags['MXP'])
self.assertEqual(self.proto.handshakes, 1)
# clean up to prevent Unclean reactor
self.proto.nop_keep_alive.stop()
self.proto._handshake_delay.cancel()
return d

View file

@ -13,14 +13,14 @@ import time
# TODO! # TODO!
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) #sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' #os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
import ev import evennia
from evennia.utils.idmapper import base as _idmapper from evennia.utils.idmapper import models as _idmapper
LOGFILE = "logs/memoryusage.log" LOGFILE = "logs/memoryusage.log"
INTERVAL = 30 # log every 30 seconds INTERVAL = 30 # log every 30 seconds
class Memplot(ev.Script): class Memplot(evennia.DefaultScript):
""" """
Describes a memory plotting action. Describes a memory plotting action.

View file

@ -1,7 +1,8 @@
from django.test import TestCase from django.test import TestCase
from mock import Mock from mock import Mock, patch, mock_open
from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login, from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login,
c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize) c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize)
import memplot
class TestDummyrunnerSettings(TestCase): class TestDummyrunnerSettings(TestCase):
@ -91,3 +92,21 @@ class TestDummyrunnerSettings(TestCase):
def test_c_move_s(self): def test_c_move_s(self):
self.assertEqual(c_moves_s(self.client), "south") self.assertEqual(c_moves_s(self.client), "south")
class TestMemPlot(TestCase):
@patch.object(memplot, "_idmapper")
@patch.object(memplot, "os")
@patch.object(memplot, "open", new_callable=mock_open, create=True)
@patch.object(memplot, "time")
def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper):
from evennia.utils.create import create_script
mocked_idmapper.cache_size.return_value = (9, 5000)
mock_time.time = Mock(return_value=6000.0)
script = create_script(memplot.Memplot)
script.db.starttime = 0.0
mocked_os.popen.read.return_value = 5000.0
script.at_repeat()
handle = mocked_open()
handle.write.assert_called_with('100.0, 0.001, 0.001, 9\n')
script.stop()

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

@ -69,8 +69,12 @@ let history_plugin = (function () {
} }
if (history_entry !== null) { if (history_entry !== null) {
// Doing a history navigation; replace the text in the input. // Performing a history navigation
inputfield.val(history_entry); // replace the text in the input and move the cursor to the end of the new value
inputfield.val('');
inputfield.blur().focus().val(history_entry);
event.preventDefault();
return true;
} }
return false; return false;

View file

@ -5,7 +5,7 @@ pypiwin32
django > 1.11, < 2.0 django > 1.11, < 2.0
twisted >= 18.0.0, < 19.0.0 twisted >= 18.0.0, < 19.0.0
pillow == 2.9.0 pillow == 5.2.0
pytz pytz
future >= 0.15.2 future >= 0.15.2
django-sekizai django-sekizai