From cccb60dd6c365e8f14eea5ca2da83b5e169f6280 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Fri, 27 Jun 2014 10:46:16 +0200
Subject: [PATCH 01/25] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2153c276f3..260e57acac 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
Evennia MUD/MU\* Creation System
================================
-*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom.
+*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](https://github.com/evennia/evennia/wiki/Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom.
http://www.evennia.com is the main hub tracking all things Evennia. The documentation wiki is found [here](https://github.com/evennia/evennia/wiki).
From 2a6cfaca7d3d3b62298acdb1c032f30121913908 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sun, 15 Jun 2014 16:17:47 +0200
Subject: [PATCH 02/25] Some cleanup of the OOB code.
---
src/server/oob_msdp.py | 10 ++-
src/server/oobhandler.py | 153 ++++++---------------------------
src/server/portal/websocket.py | 2 +-
3 files changed, 34 insertions(+), 131 deletions(-)
diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py
index 9bafa9b79c..eb694164f2 100644
--- a/src/server/oob_msdp.py
+++ b/src/server/oob_msdp.py
@@ -134,7 +134,9 @@ class TrackerBase(object):
pass
-class OOBFieldTracker(TrackerBase):
+# Tracker objects stored on objects when using the MSDP report command
+
+class ReportFieldTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
a named database field changes. The TrackerHandler calls this with
@@ -162,7 +164,7 @@ class OOBFieldTracker(TrackerBase):
new_value, *args, **kwargs)
-class OOBAttributeTracker(TrackerBase):
+class ReportAttributeTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
the Attribute updates. Since the field here is always "db_key",
@@ -288,10 +290,10 @@ def report(oobhandler, session, *args, **kwargs):
if key:
if key.startswith("db_"):
oobhandler.track_field(obj, session.sessid,
- key, OOBFieldTracker)
+ key, ReportFieldTracker)
else: # assume attribute
oobhandler.track_attribute(obj, session.sessid,
- key, OOBAttributeTracker)
+ key, ReportAttributeTracker)
def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py
index d4f5eeda77..0e3f845db8 100644
--- a/src/server/oobhandler.py
+++ b/src/server/oobhandler.py
@@ -61,9 +61,17 @@ _OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
+ "Error wrapper"
session.msg(oob=("send", {"ERROR": errmsg}))
_OOB_ERROR = oob_error
+
+#
+# TrackerHandler is assigned to objects that should notify themselves to
+# the OOB system when some property changes. This is never assigned manually
+# but automatically through the OOBHandler.
+#
+
class TrackerHandler(object):
"""
This object is dynamically assigned to objects whenever one of its fields
@@ -123,6 +131,8 @@ class TrackerHandler(object):
logger.log_trace()
+# Tracker loaded by the TrackerHandler
+
class TrackerBase(object):
"""
Base class for OOB Tracker objects.
@@ -138,84 +148,7 @@ class TrackerBase(object):
"Called when tracker is removed"
pass
-
-#class _RepeaterScript(Script):
-# """
-# Repeating and subscription-enabled script for triggering OOB
-# functions. Maintained in a _RepeaterPool.
-# """
-# def at_script_creation(self):
-# "Called when script is initialized"
-# self.key = "oob_func"
-# self.desc = "OOB functionality script"
-# self.persistent = False # oob scripts should always be non-persistent
-# self.ndb.subscriptions = {}
-#
-# def at_repeat(self):
-# """
-# Calls subscriptions every self.interval seconds
-# """
-# for (func_key, sessid, interval, args, kwargs) in self.ndb.subscriptions.values():
-# session = SESSIONS.session_from_sessid(sessid)
-# OOB_HANDLER.execute_cmd(session, func_key, *args, **kwargs)
-#
-# def subscribe(self, store_key, sessid, func_key, interval, *args, **kwargs):
-# """
-# Sign up a subscriber to this oobfunction. Subscriber is
-# a database object with a dbref.
-# """
-# self.ndb.subscriptions[store_key] = (func_key, sessid, interval, args, kwargs)
-#
-# def unsubscribe(self, store_key):
-# """
-# Unsubscribe from oobfunction. Returns True if removal was
-# successful, False otherwise
-# """
-# self.ndb.subscriptions.pop(store_key, None)
-#
-#
-#class _RepeaterPool(object):
-# """
-# This maintains a pool of _RepeaterScript scripts, ordered one per
-# interval. It will automatically cull itself once a given interval's
-# script has no more subscriptions.
-#
-# This is used and accessed from oobhandler.repeat/unrepeat
-# """
-#
-# def __init__(self):
-# self.scripts = {}
-#
-# def add(self, store_key, sessid, func_key, interval, *args, **kwargs):
-# """
-# Add a new tracking
-# """
-# if interval not in self.scripts:
-# # if no existing interval exists, create new script to fill the gap
-# new_tracker = create_script(_RepeaterScript,
-# key="oob_repeater_%is" % interval, interval=interval)
-# self.scripts[interval] = new_tracker
-# self.scripts[interval].subscribe(store_key, sessid, func_key,
-# interval, *args, **kwargs)
-#
-# def remove(self, store_key, interval):
-# """
-# Remove tracking
-# """
-# if interval in self.scripts:
-# self.scripts[interval].unsubscribe(store_key)
-# if len(self.scripts[interval].ndb.subscriptions) == 0:
-# # no more subscriptions for this interval. Clean out the script.
-# self.scripts[interval].stop()
-#
-# def stop(self):
-# """
-# Stop all scripts in pool. This is done at server reload since
-# restoring the pool will automatically re-populate the pool.
-# """
-# for script in self.scripts.values():
-# script.stop()
-
+# Ticker of auto-updating objects
class OOBTicker(Ticker):
"""
@@ -244,6 +177,7 @@ class OOBTickerPool(TickerPool):
class OOBTickerHandler(TickerHandler):
ticker_pool_class = OOBTickerPool
+
# Main OOB Handler
class OOBHandler(object):
@@ -258,8 +192,6 @@ class OOBHandler(object):
"""
self.sessionhandler = SESSIONS
self.oob_tracker_storage = {}
- #self.oob_repeat_storage = {}
- #self.oob_tracker_pool = _RepeaterPool()
self.tickerhandler = OOBTickerHandler("oob_ticker_storage")
def save(self):
@@ -272,11 +204,6 @@ class OOBHandler(object):
ServerConfig.objects.conf(key="oob_tracker_storage",
value=dbserialize(self.oob_tracker_storage))
self.tickerhandler.save()
- #if self.oob_repeat_storage:
- # #print "saved repeat_storage:", self.oob_repeat_storage
- # ServerConfig.objects.conf(key="oob_repeat_storage",
- # value=dbserialize(self.oob_repeat_storage))
- #self.oob_tracker_pool.stop()
def restore(self):
"""
@@ -290,27 +217,17 @@ class OOBHandler(object):
#print "recovered from tracker_storage:", self.oob_tracker_storage
for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values():
self.track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs)
- # make sure to purce the storage
+ # make sure to purge the storage
ServerConfig.objects.conf(key="oob_tracker_storage", delete=True)
-
self.tickerhandler.restore()
- #repeat_storage = ServerConfig.objects.conf(key="oob_repeat_storage")
- #if repeat_storage:
- # self.oob_repeat_storage = dbunserialize(repeat_storage)
- # #print "recovered from repeat_storage:", self.oob_repeat_storage
- # for (obj, sessid, func_key, interval, args, kwargs) in self.oob_repeat_storage.values():
- # self.repeat(unpack_dbobj(obj), sessid, func_key, interval, *args, **kwargs)
- # # make sure to purge the storage
- # ServerConfig.objects.conf(key="oob_repeat_storage", delete=True)
-
- def track(self, obj, sessid, fieldname, trackerclass, *args, **kwargs):
+ def _track(self, obj, sessid, propname, trackerclass, *args, **kwargs):
"""
Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args,
kwargs will be used to initialize the OOB hook before adding
it to obj.
- If property_key is not given, but the OOB has a class property
- property_name, this will be used as the property name when assigning
+ If propname is not given, but the OOB has a class property
+ named as propname, this will be used as the property name when assigning
the OOB to obj, otherwise tracker_key is used as the property name.
"""
try:
@@ -322,15 +239,15 @@ class OOBHandler(object):
# assign trackerhandler to object
_SA(obj, "_trackerhandler", TrackerHandler(obj))
# initialize object
- tracker = trackerclass(self, fieldname, sessid, *args, **kwargs)
- _GA(obj, "_trackerhandler").add(fieldname, tracker)
+ tracker = trackerclass(self, propname, sessid, *args, **kwargs)
+ _GA(obj, "_trackerhandler").add(propname, tracker)
# store calling arguments as a pickle for retrieval later
obj_packed = pack_dbobj(obj)
- storekey = (obj_packed, sessid, fieldname)
- stored = (obj_packed, sessid, fieldname, trackerclass, args, kwargs)
+ storekey = (obj_packed, sessid, propname)
+ stored = (obj_packed, sessid, propname, trackerclass, args, kwargs)
self.oob_tracker_storage[storekey] = stored
- def untrack(self, obj, sessid, fieldname, trackerclass, *args, **kwargs):
+ def _untrack(self, obj, sessid, propname, trackerclass, *args, **kwargs):
"""
Remove the OOB from obj. If oob implements an
at_delete hook, this will be called with args, kwargs
@@ -342,11 +259,11 @@ class OOBHandler(object):
try:
# call at_delete hook
- _GA(obj, "_trackerhandler").remove(fieldname, trackerclass, *args, **kwargs)
+ _GA(obj, "_trackerhandler").remove(propname, trackerclass, *args, **kwargs)
except AttributeError:
pass
# remove the pickle from storage
- store_key = (pack_dbobj(obj), sessid, fieldname)
+ store_key = (pack_dbobj(obj), sessid, propname)
self.oob_tracker_storage.pop(store_key, None)
def get_all_tracked(self, session):
@@ -363,14 +280,14 @@ class OOBHandler(object):
"""
# all database field names starts with db_*
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
- self.track(obj, sessid, field_name, trackerclass)
+ self._track(obj, sessid, field_name, trackerclass)
def untrack_field(self, obj, sessid, field_name):
"""
Shortcut for untracking a database field. Uses OOBTracker by defualt
"""
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
- self.untrack(obj, sessid, field_name)
+ self._untrack(obj, sessid, field_name)
def track_attribute(self, obj, sessid, attr_name, trackerclass):
"""
@@ -385,7 +302,7 @@ class OOBHandler(object):
pass
attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
if attrobj:
- self.track(attrobj, sessid, "db_value", trackerclass, attr_name)
+ self._track(attrobj, sessid, "db_value", trackerclass, attr_name)
def untrack_attribute(self, obj, sessid, attr_name, trackerclass):
"""
@@ -397,7 +314,7 @@ class OOBHandler(object):
pass
attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
if attrobj:
- self.untrack(attrobj, sessid, attr_name, trackerclass)
+ self._untrack(attrobj, sessid, attr_name, trackerclass)
def repeat(self, obj, sessid, func_key, interval=20, *args, **kwargs):
"""
@@ -407,29 +324,13 @@ class OOBHandler(object):
"""
if not func_key in _OOB_FUNCS:
raise KeyError("%s is not a valid OOB function name.")
- #try:
- # obj = obj.dbobj
- #except AttributeError:
- # pass
self.tickerhandler.add(self, obj, interval, func_key=func_key, sessid=sessid, *args, **kwargs)
- #store_obj = pack_dbobj(obj)
- #store_key = (store_obj, sessid, func_key, interval)
- ## prepare to store
- #self.oob_repeat_storage[store_key] = (store_obj, sessid, func_key, interval, args, kwargs)
- #self.oob_tracker_pool.add(store_key, sessid, func_key, interval, *args, **kwargs)
def unrepeat(self, obj, sessid, func_key, interval=20):
"""
Stop a repeating action
"""
self.tickerhandler.remove(self, obj, interval)
- #try:
- # obj = obj.dbobj
- #except AttributeError:
- # pass
- #store_key = (pack_dbobj(obj), sessid, func_key, interval)
- #self.oob_tracker_pool.remove(store_key, interval)
- #self.oob_repeat_storage.pop(store_key, None)
def msg(self, sessid, funcname, *args, **kwargs):
"Shortcut to relay oob data back to portal. Used by oob functions."
diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket.py
index e457368db3..0c3b3f5304 100644
--- a/src/server/portal/websocket.py
+++ b/src/server/portal/websocket.py
@@ -84,7 +84,7 @@ class WebSocketProtocol(Protocol, Session):
for (key, argstuple) in oobdata.items():
args = argstuple[0] if argstuple else []
kwargs = argstuple[1] if len(argstuple) > 1 else {}
- self.data_in(oob=(key, args, kwargs))
+ self.data_in(text=None, oob=(key, args, kwargs))
except Exception:
log_trace("Websocket malformed OOB request: %s" % string)
else:
From ede6634081a90b81c628a4b9ecb35dfd8931c1ce Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sun, 15 Jun 2014 21:58:53 +0200
Subject: [PATCH 03/25] Converted webclient to use websockets on port 8001.
Ideally one would make it so the ajax and websocket clients work under the
same django wrapper, but for now this functionality is elusive.
---
src/server/portal/portal.py | 7 +-
src/server/portal/websocket.py | 1 +
src/settings_default.py | 14 +-
...webclient.js => evennia_ajax_webclient.js} | 0
.../javascript/evennia_websocket_webclient.js | 266 ++++++++++++++++++
src/web/templates/admin/index.html | 26 +-
src/web/templates/prosimii/webclient.html | 23 +-
src/web/urls.py | 4 +-
src/web/utils/general_context.py | 15 +-
src/web/webclient/views.py | 5 +-
src/web/website/views.py | 2 +-
11 files changed, 326 insertions(+), 37 deletions(-)
rename src/web/media/javascript/{evennia_webclient.js => evennia_ajax_webclient.js} (100%)
create mode 100644 src/web/media/javascript/evennia_websocket_webclient.js
diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py
index c40bf5dedc..366da88009 100644
--- a/src/server/portal/portal.py
+++ b/src/server/portal/portal.py
@@ -262,6 +262,12 @@ if WEBSERVER_ENABLED:
web_root.putChild("webclientdata", webclient)
webclientstr = "/client"
+ #from src.server.portal import websocket
+ #factory = protocol.ServerFactory()
+ #websocketclient = websocket.WebSocketProtocol()
+ #websocketclient.sessionhandler = PORTAL_SESSIONS
+ #web_root.putChild("websocket", websocketclient)
+
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
proxy_service = internet.TCPServer(proxyport,
web_root,
@@ -270,7 +276,6 @@ if WEBSERVER_ENABLED:
PORTAL.services.addService(proxy_service)
print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport)
-
if WEBSOCKET_ENABLED:
# websocket support is experimental!
diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket.py
index 0c3b3f5304..0040921993 100644
--- a/src/server/portal/websocket.py
+++ b/src/server/portal/websocket.py
@@ -37,6 +37,7 @@ class WebSocketProtocol(Protocol, Session):
"""
This is called when the connection is first established
"""
+
def connectionMade(self):
"""
This is called when the connection is first established.
diff --git a/src/settings_default.py b/src/settings_default.py
index 68ed71c333..f03b3544a0 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -61,6 +61,14 @@ WEBSERVER_THREADPOOL_LIMITS = (1, 20)
# Start the evennia ajax client on /webclient
# (the webserver must also be running)
WEBCLIENT_ENABLED = True
+# Activate Websocket support. If this is on, the default webclient will use this
+# before going for the ajax implementation
+WEBSOCKET_ENABLED = True
+# Ports to use for Websockets. If this is changed, you must also update
+# src/web/media/javascript/evennia_websocket_webclient.js to match.
+WEBSOCKET_PORTS = [8001]
+# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
+WEBSOCKET_INTERFACES = ['0.0.0.0']
# Activate SSH protocol (SecureShell)
SSH_ENABLED = False
# Ports to use for SSH
@@ -73,12 +81,6 @@ SSL_ENABLED = False
SSL_PORTS = [4001]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
SSL_INTERFACES = ['0.0.0.0']
-# Activate Websocket support
-WEBSOCKET_ENABLED = False
-# Ports to use for Websockets
-WEBSOCKET_PORTS = [8021]
-# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
-WEBSOCKET_INTERFACES = ['0.0.0.0']
# The path that contains this settings.py file (no trailing slash).
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Path to the src directory containing the bulk of the codebase's code.
diff --git a/src/web/media/javascript/evennia_webclient.js b/src/web/media/javascript/evennia_ajax_webclient.js
similarity index 100%
rename from src/web/media/javascript/evennia_webclient.js
rename to src/web/media/javascript/evennia_ajax_webclient.js
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
new file mode 100644
index 0000000000..b58c605923
--- /dev/null
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -0,0 +1,266 @@
+/*
+
+Evennia websocket webclient (javascript component)
+
+The client is composed of several parts:
+ templates/webclient.html - the main page
+ webclient/views.py - the django view serving the template (based on urls.py pattern)
+ src/server/portal/websockets.py - the server component talking to the client
+ this file - the javascript component handling dynamic content
+
+This implements an mud client for use with Evennia, using jQuery
+for simplicity.
+
+messages sent to the client is one of three modes:
+ OOB(func,(args), func,(args), ...) - OOB command executions
+ text - any other text is considered a normal text to echo
+
+*/
+
+// jQuery must be imported by the calling html page before this script
+// There are plenty of help on using the jQuery library on http://jquery.com/
+
+// Server communications
+// Set this to the value matching settings.WEBSOCKET_PORTS
+var wsurl = "ws://localhost:8001";
+
+function webclient_init(){
+ websocket = new WebSocket(wsurl);
+ websocket.onopen = function(evt) { onOpen(evt) };
+ websocket.onclose = function(evt) { onClose(evt) };
+ websocket.onmessage = function(evt) { onMessage(evt) };
+ websocket.onerror = function(evt) { onError(evt) };
+}
+
+function onOpen(evt) {
+ $("#connecting").remove(); // remove the "connecting ..." message
+ msg_display("sys", "Using websockets - connected to " + wsurl + ".")
+
+ setTimeout(function () {
+ $("#playercount").fadeOut('slow', webclient_set_sizes);
+ }, 10000);
+}
+
+function onClose(evt) {
+ CLIENT_HASH = 0;
+ alert("Mud client connection was closed cleanly.");
+}
+
+function onMessage(evt) {
+ var inmsg = evt.data
+ if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
+ // dynamically call oob methods, if available
+ try {var oobtuples = JSON.parse(inmsg.slice(3));} // everything after OOB }
+ catch(err) {
+ // not JSON packed - a normal text
+ msg_display('out', inmsg);
+ return;
+ }
+
+ for (var oobtuple in oobtuples) {
+ try { window[oobtuple[0]](oobtuple[1]) }
+ catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "!") }
+ }
+ }
+ else {
+ // normal message
+ msg_display('out', inmsg); }
+}
+
+function onError(evt) {
+ msg_display('err', "Error: Server returned an error. Try reloading the page.");
+}
+
+function doSend(){
+ outmsg = $("#inputfield").val();
+ history_add(outmsg);
+ HISTORY_POS = 0;
+ $('#inputform')[0].reset(); // clear input field
+ websocket.send(outmsg);
+}
+
+function doOOB(ooblist){
+ // Takes an array on form [funcname, [args], funcname, [args], ... ]
+ var oobmsg = JSON.stringify(ooblist);
+ websocket.send("OOB" + oobmsg);
+}
+
+//
+// Display messages
+
+function msg_display(type, msg){
+ // Add a div to the message window.
+ // type gives the class of div to use.
+ $("#messagewindow").append(
+ "
"+ msg +"
");
+ // scroll message window to bottom
+ $('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight});
+}
+
+// Input history mechanism
+
+var HISTORY_MAX_LENGTH = 21
+var HISTORY = new Array();
+HISTORY[0] = '';
+var HISTORY_POS = 0;
+
+function history_step_back() {
+ // step backwards in history stack
+ HISTORY_POS = Math.min(++HISTORY_POS, HISTORY.length-1);
+ return HISTORY[HISTORY.length-1 - HISTORY_POS];
+}
+function history_step_fwd() {
+ // step forward in history stack
+ HISTORY_POS = Math.max(--HISTORY_POS, 0);
+ return HISTORY[HISTORY.length-1 - HISTORY_POS];
+}
+function history_add(input) {
+ // add an entry to history
+ if (input != HISTORY[HISTORY.length-1]) {
+ if (HISTORY.length >= HISTORY_MAX_LENGTH) {
+ HISTORY.shift(); // kill oldest history entry
+ }
+ HISTORY[HISTORY.length-1] = input;
+ HISTORY[HISTORY.length] = '';
+ }
+}
+
+// Catching keyboard shortcuts
+
+$.fn.appendCaret = function() {
+ /* jQuery extension that will forward the caret to the end of the input, and
+ won't harm other elements (although calling this on multiple inputs might
+ not have the expected consequences).
+
+ Thanks to
+ http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
+ for the good starting point. */
+ return this.each(function() {
+ var range,
+ // Index at where to place the caret.
+ end,
+ self = this;
+
+ if (self.setSelectionRange) {
+ // other browsers
+ end = self.value.length;
+ self.focus();
+ // NOTE: Need to delay the caret movement until after the callstack.
+ setTimeout(function() {
+ self.setSelectionRange(end, end);
+ }, 0);
+ }
+ else if (self.createTextRange) {
+ // IE
+ end = self.value.length - 1;
+ range = self.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', end);
+ range.moveStart('character', end);
+ // NOTE: I haven't tested to see if IE has the same problem as
+ // W3C browsers seem to have in this context (needing to fire
+ // select after callstack).
+ range.select();
+ }
+ });
+};
+$.fn.appendCaret = function() {
+ /* jQuery extension that will forward the caret to the end of the input, and
+ won't harm other elements (although calling this on multiple inputs might
+ not have the expected consequences).
+
+ Thanks to
+ http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
+ for the good starting point. */
+ return this.each(function() {
+ var range,
+ // Index at where to place the caret.
+ end,
+ self = this;
+
+ if (self.setSelectionRange) {
+ // other browsers
+ end = self.value.length;
+ self.focus();
+ // NOTE: Need to delay the caret movement until after the callstack.
+ setTimeout(function() {
+ self.setSelectionRange(end, end);
+ }, 0);
+ }
+ else if (self.createTextRange) {
+ // IE
+ end = self.value.length - 1;
+ range = self.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', end);
+ range.moveStart('character', end);
+ // NOTE: I haven't tested to see if IE has the same problem as
+ // W3C browsers seem to have in this context (needing to fire
+ // select after callstack).
+ range.select();
+ }
+ });
+};
+
+$(document).keydown( function(event) {
+ // Get the pressed key (normalized by jQuery)
+ var code = event.which,
+ inputField = $("#inputfield");
+
+ // always focus input field no matter which key is pressed
+ inputField.focus();
+
+ // Special keys recognized by client
+
+ //msg_display("out", "key code pressed: " + code); // debug
+
+ if (code == 13) { // Enter Key
+ doSend();
+ event.preventDefault();
+ }
+ else {
+ if (code == 38) { // arrow up 38
+ inputField.val(history_step_back()).appendCaret();
+ }
+ else if (code == 40) { // arrow down 40
+ inputField.val(history_step_fwd()).appendCaret();
+ }
+ }
+});
+
+// handler to avoid double-clicks until the ajax request finishes
+//$("#inputsend").one("click", webclient_input)
+
+function webclient_set_sizes() {
+ // Sets the size of the message window
+ var win_h = $(document).height();
+ //var win_w = $('#wrapper').width();
+ var inp_h = $('#inputform').outerHeight(true);
+ //var inp_w = $('#inputsend').outerWidth(true);
+
+ $("#messagewindow").css({'height': win_h - inp_h - 1});
+ //$("#inputfield").css({'width': win_w - inp_w - 20});
+}
+
+
+// Callback function - called when page has finished loading (gets things going)
+$(document).ready(function(){
+ // remove the "no javascript" warning, since we obviously have javascript
+ $('#noscript').remove();
+ // set sizes of elements and reposition them
+ webclient_set_sizes();
+ // a small timeout to stop 'loading' indicator in Chrome
+ setTimeout(function () {
+ webclient_init();
+ }, 500);
+ // set an idle timer to avoid proxy servers to time out on us (every 3 minutes)
+ setInterval(function() {
+ webclient_input("idle", true);
+ }, 60000*3);
+});
+
+// Callback function - called when the browser window resizes
+$(window).resize(webclient_set_sizes);
+
+// Callback function - called when page is closed or moved away from.
+//$(window).bind("beforeunload", webclient_close);
diff --git a/src/web/templates/admin/index.html b/src/web/templates/admin/index.html
index a7487cd49d..7a4f0a5295 100644
--- a/src/web/templates/admin/index.html
+++ b/src/web/templates/admin/index.html
@@ -14,12 +14,12 @@
{% if app_list %}
- {% for app in app_list %}
+ {% for app in app_list %}
{% if app.name in evennia_userapps %}
{% if app.name == 'Players' %}
-
Admin
+
Admin
Players are the out-of-character representation of a
game account. A Player can potentially control any number of
in-game character Objects (depending on game).
- {% for app in app_list %}
+ {% for app in app_list %}
{% if app.name in evennia_entityapps %}
- {% if app.name == 'Comms' %}
+ {% if app.name == 'Comms' %}
This defines entities that has an in-game precense or
- effect of some kind.
diff --git a/src/web/urls.py b/src/web/urls.py
index 5ca3bf8a41..9d9d1844c3 100755
--- a/src/web/urls.py
+++ b/src/web/urls.py
@@ -44,8 +44,8 @@ urlpatterns = patterns('',
# favicon
url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')),
- # ajax stuff
- url(r'^webclient/',include('src.web.webclient.urls')),
+ # webclient stuff
+ url(r'^webclient/', include('src.web.webclient.urls')),
)
# This sets up the server if the user want to run the Django
diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py
index 2b9304e94a..421bf9587a 100644
--- a/src/web/utils/general_context.py
+++ b/src/web/utils/general_context.py
@@ -1,9 +1,9 @@
#
-# This file defines global variables that will always be
+# This file defines global variables that will always be
# available in a view context without having to repeatedly
-# include it. For this to work, this file is included in
-# the settings file, in the TEMPLATE_CONTEXT_PROCESSORS
-# tuple.
+# include it. For this to work, this file is included in
+# the settings file, in the TEMPLATE_CONTEXT_PROCESSORS
+# tuple.
#
from django.db import models
@@ -19,8 +19,8 @@ except AttributeError:
SERVER_VERSION = get_evennia_version()
-# Setup lists of the most relevant apps so
-# the adminsite becomes more readable.
+# Setup lists of the most relevant apps so
+# the adminsite becomes more readable.
PLAYER_RELATED = ['Players']
GAME_ENTITIES = ['Objects', 'Scripts', 'Comms', 'Help']
@@ -44,5 +44,6 @@ def general_context(request):
'evennia_setupapps': GAME_SETUP,
'evennia_connectapps': CONNECTIONS,
'evennia_websiteapps':WEBSITE,
- "webclient_enabled" : settings.WEBCLIENT_ENABLED
+ "webclient_enabled" : settings.WEBCLIENT_ENABLED,
+ "websocket_enabled" : settings.WEBSOCKET_ENABLED
}
diff --git a/src/web/webclient/views.py b/src/web/webclient/views.py
index 3d097b6935..d74ff4f5e2 100644
--- a/src/web/webclient/views.py
+++ b/src/web/webclient/views.py
@@ -8,7 +8,7 @@ page and serve it eventual static content.
from django.shortcuts import render_to_response, redirect
from django.template import RequestContext
from django.conf import settings
-from src.server.sessionhandler import SESSIONS
+from src.players.models import PlayerDB
def webclient(request):
"""
@@ -21,8 +21,9 @@ def webclient(request):
print "Called from port 8000!"
#return redirect("http://localhost:8001/webclient/", permanent=True)
+ nsess = len(PlayerDB.objects.get_connected_players()) or "none"
# as an example we send the number of connected players to the template
- pagevars = {'num_players_connected': SESSIONS.player_count()}
+ pagevars = {'num_players_connected': nsess}
context_instance = RequestContext(request)
return render_to_response('webclient.html', pagevars, context_instance)
diff --git a/src/web/website/views.py b/src/web/website/views.py
index a0af399ddc..2ab73c3d19 100644
--- a/src/web/website/views.py
+++ b/src/web/website/views.py
@@ -33,7 +33,7 @@ def page_index(request):
nplyrs_conn_recent = len(recent_users) or "none"
nplyrs = PlayerDB.objects.num_total_players() or "none"
nplyrs_reg_recent = len(PlayerDB.objects.get_recently_created_players()) or "none"
- nsess = len(PlayerDB.objects.get_connected_players()) or "noone"
+ nsess = len(PlayerDB.objects.get_connected_players()) or "none"
nobjs = ObjectDB.objects.all().count()
nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()
From e6c1ab3df82095da2f703e7627856ac39c7812f4 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sun, 15 Jun 2014 23:14:06 +0200
Subject: [PATCH 04/25] First make of a testing method for oob
---
src/web/media/javascript/evennia_websocket_webclient.js | 9 +++++++--
src/web/templates/prosimii/webclient.html | 2 +-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index b58c605923..17d7c50f1e 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -50,7 +50,7 @@ function onMessage(evt) {
var inmsg = evt.data
if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
// dynamically call oob methods, if available
- try {var oobtuples = JSON.parse(inmsg.slice(3));} // everything after OOB }
+ try {var oobtuples = JSON.parse(inmsg.slice(4));} // everything after OOB }
catch(err) {
// not JSON packed - a normal text
msg_display('out', inmsg);
@@ -76,7 +76,12 @@ function doSend(){
history_add(outmsg);
HISTORY_POS = 0;
$('#inputform')[0].reset(); // clear input field
- websocket.send(outmsg);
+
+ if (outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
+ // test OOB messaging
+ doOOB(JSON.parse(outmsg.slice(5))); }
+ else {
+ websocket.send(outmsg); }
}
function doOOB(ooblist){
diff --git a/src/web/templates/prosimii/webclient.html b/src/web/templates/prosimii/webclient.html
index b0d815170f..ba7769af23 100644
--- a/src/web/templates/prosimii/webclient.html
+++ b/src/web/templates/prosimii/webclient.html
@@ -51,7 +51,7 @@
From a9cf0814948fb5d2742bb59ed3e35e9ab65e31c5 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sun, 15 Jun 2014 23:17:27 +0200
Subject: [PATCH 05/25] First make of a testing method for oob
---
src/server/oob_msdp.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py
index eb694164f2..d57531c690 100644
--- a/src/server/oob_msdp.py
+++ b/src/server/oob_msdp.py
@@ -311,3 +311,6 @@ def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, key)
+def echo(oobhandler, session, *args, **kwargs):
+ "Test function, returning the args, kwargs"
+ session.msg(oob=("send", args, kwargs))
From ca1e36da5f6dfddef337f6e059e14ced214b99a9 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Wed, 25 Jun 2014 23:35:21 +0200
Subject: [PATCH 06/25] First working test version of oob support in the
websocket client.
---
src/server/oob_msdp.py | 15 ++++----
src/server/oobhandler.py | 2 +-
src/server/portal/websocket.py | 8 ++---
src/server/sessionhandler.py | 15 +++++---
.../javascript/evennia_websocket_webclient.js | 34 ++++++++++++++-----
5 files changed, 48 insertions(+), 26 deletions(-)
diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py
index d57531c690..6b474fde79 100644
--- a/src/server/oob_msdp.py
+++ b/src/server/oob_msdp.py
@@ -198,7 +198,7 @@ class ReportAttributeTracker(TrackerBase):
# eventual args/kwargs. All functions defined globally in this
# module will be made available to call by the oobhandler. Use
# _funcname if you want to exclude one. To allow for python-names
-# like "list" here, these properties are read as being case-insensitive.
+# like "list" here, these properties are case-insensitive.
#
# All OOB commands must be on the form
# cmdname(oobhandler, session, *args, **kwargs)
@@ -212,7 +212,7 @@ def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"""
session.msg(oob=("send", {"ERROR": errmsg}))
-def list(oobhandler, session, mode, *args, **kwargs):
+def LIST(oobhandler, session, mode, *args, **kwargs):
"""
List available properties. Mode is the type of information
desired:
@@ -260,7 +260,7 @@ def list(oobhandler, session, mode, *args, **kwargs):
session.msg(oob=("list", ("unsupported mode",)))
-def send(oobhandler, session, *args, **kwargs):
+def SEND(oobhandler, session, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session. vartype can be one of
@@ -278,7 +278,7 @@ def send(oobhandler, session, *args, **kwargs):
session.msg(oob=("send", ret))
-def report(oobhandler, session, *args, **kwargs):
+def REPORT(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
vartype is one of "prop" (database fields) or "attr" (attributes)
@@ -296,7 +296,7 @@ def report(oobhandler, session, *args, **kwargs):
key, ReportAttributeTracker)
-def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
+def UNREPORT(oobhandler, session, vartype="prop", *args, **kwargs):
"""
This removes tracking for the given data given in *args.
vartype is one of of "prop" or "attr".
@@ -311,6 +311,7 @@ def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, key)
-def echo(oobhandler, session, *args, **kwargs):
+def ECHO(oobhandler, session, *args, **kwargs):
"Test function, returning the args, kwargs"
- session.msg(oob=("send", args, kwargs))
+ args = ["Return echo:"] + list(args)
+ session.msg(oob=("echo", args, kwargs))
diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py
index 0e3f845db8..fea7a5642a 100644
--- a/src/server/oobhandler.py
+++ b/src/server/oobhandler.py
@@ -347,7 +347,7 @@ class OOBHandler(object):
using *args and **kwargs
"""
try:
- #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
+ print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc(self, session, *args, **kwargs)
except KeyError,e:
diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket.py
index 0040921993..7060e79249 100644
--- a/src/server/portal/websocket.py
+++ b/src/server/portal/websocket.py
@@ -73,7 +73,7 @@ class WebSocketProtocol(Protocol, Session):
prefix.
OOB - This is an Out-of-band instruction. If so,
the remaining string should be a json-packed
- string on the form {oobfuncname: [[args], {kwargs}], ...}
+ string on the form {oobfuncname: [args, ], ...}
any other prefix (or lack of prefix) is considered
plain text data, to be treated like a game
input command.
@@ -82,10 +82,8 @@ class WebSocketProtocol(Protocol, Session):
string = string[3:]
try:
oobdata = json.loads(string)
- for (key, argstuple) in oobdata.items():
- args = argstuple[0] if argstuple else []
- kwargs = argstuple[1] if len(argstuple) > 1 else {}
- self.data_in(text=None, oob=(key, args, kwargs))
+ for (key, args) in oobdata.items():
+ self.data_in(text=None, oob=(key, args))
except Exception:
log_trace("Websocket malformed OOB request: %s" % string)
else:
diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py
index bfb7efb313..ec69d35c6e 100644
--- a/src/server/sessionhandler.py
+++ b/src/server/sessionhandler.py
@@ -104,7 +104,14 @@ class SessionHandler(object):
"""
Helper method for each session to use to parse oob structures
(The 'oob' kwarg of the msg() method)
+ ((cmdname, (args), {}), ...)
+
+
+ Allowed oob structures are:
allowed oob structures are
+
+
+
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))
@@ -134,10 +141,10 @@ class SessionHandler(object):
return (oobstruct[0].lower(), (), dict(oobstruct[1]))
elif isinstance(oobstruct[1], (tuple, list)):
# cmdname, (args,)
- return (oobstruct[0].lower(), tuple(oobstruct[1]), {})
+ return (oobstruct[0].lower(), list(oobstruct[1]), {})
else:
# cmdname, (args,), {kwargs}
- return (oobstruct[0].lower(), tuple(oobstruct[1]), dict(oobstruct[2]))
+ return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2]))
if hasattr(oobstruct, "__iter__"):
# differentiate between (cmdname, cmdname),
@@ -145,12 +152,12 @@ class SessionHandler(object):
# (cmdname,args,kwargs), ...)
if oobstruct and isinstance(oobstruct[0], basestring):
- return (tuple(_parse(oobstruct)),)
+ return (list(_parse(oobstruct)),)
else:
out = []
for oobpart in oobstruct:
out.append(_parse(oobpart))
- return (tuple(out),)
+ return (list(out),)
return (_parse(oobstruct),)
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index 17d7c50f1e..2b5e46f554 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -25,6 +25,7 @@ messages sent to the client is one of three modes:
var wsurl = "ws://localhost:8001";
function webclient_init(){
+ // initializing the client once the html page has loaded
websocket = new WebSocket(wsurl);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
@@ -33,6 +34,7 @@ function webclient_init(){
}
function onOpen(evt) {
+ // client is just connecting
$("#connecting").remove(); // remove the "connecting ..." message
msg_display("sys", "Using websockets - connected to " + wsurl + ".")
@@ -42,24 +44,25 @@ function onOpen(evt) {
}
function onClose(evt) {
+ // client is closing
CLIENT_HASH = 0;
alert("Mud client connection was closed cleanly.");
}
function onMessage(evt) {
+ // outgoing message from server
var inmsg = evt.data
if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
// dynamically call oob methods, if available
- try {var oobtuples = JSON.parse(inmsg.slice(4));} // everything after OOB }
+ try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB }
catch(err) {
// not JSON packed - a normal text
- msg_display('out', inmsg);
+ msg_display('out', err + " " + inmsg);
return;
}
-
- for (var oobtuple in oobtuples) {
- try { window[oobtuple[0]](oobtuple[1]) }
- catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "!") }
+ for (var ind in oobarray) {
+ try { window[oobarray[ind][0]](oobarray[ind][1]) }
+ catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") }
}
}
else {
@@ -68,10 +71,12 @@ function onMessage(evt) {
}
function onError(evt) {
+ // client error message
msg_display('err', "Error: Server returned an error. Try reloading the page.");
}
function doSend(){
+ // sending data from client to server
outmsg = $("#inputfield").val();
history_add(outmsg);
HISTORY_POS = 0;
@@ -84,12 +89,23 @@ function doSend(){
websocket.send(outmsg); }
}
-function doOOB(ooblist){
- // Takes an array on form [funcname, [args], funcname, [args], ... ]
- var oobmsg = JSON.stringify(ooblist);
+function doOOB(oobdict){
+ // Handle OOB communication from client side
+ // Takes a dict on form {funcname:[args], funcname: [args], ... ]
+ msg_display("out", "into doOOB: " + oobdict)
+ msg_display("out", "stringify: " + JSON.stringify(oobdict))
+ var oobmsg = JSON.stringify(oobdict);
websocket.send("OOB" + oobmsg);
}
+
+//
+// OOB functions
+//
+
+function echo(message) {
+ msg_display("out", "ECHO return: " + message) }
+
//
// Display messages
From d59500f57496e980e451c9e6b3d7651f4eddb512 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Thu, 26 Jun 2014 23:37:03 +0200
Subject: [PATCH 07/25] Updated to a working websocket implementation of
webclient.
---
src/server/portal/portal.py | 58 ++++---
.../{websocket.py => websocket_client.py} | 18 ++-
src/settings_default.py | 24 +--
.../javascript/evennia_websocket_webclient.js | 142 +++++++++---------
src/web/templates/prosimii/webclient.html | 3 +-
src/web/utils/general_context.py | 9 +-
6 files changed, 133 insertions(+), 121 deletions(-)
rename src/server/portal/{websocket.py => websocket_client.py} (89%)
diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py
index 366da88009..8c4a812199 100644
--- a/src/server/portal/portal.py
+++ b/src/server/portal/portal.py
@@ -42,20 +42,21 @@ TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
-WEBSOCKET_PORTS = settings.WEBSOCKET_PORTS
+WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
-WEBSOCKET_INTERFACES = settings.WEBSOCKET_INTERFACES
+WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE
+WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
-WEBSOCKET_ENABLED = settings.WEBSOCKET_ENABLED and WEBSOCKET_PORTS and WEBSOCKET_INTERFACES
+WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
@@ -257,16 +258,31 @@ if WEBSERVER_ENABLED:
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from src.server.portal.webclient import WebClient
+
webclient = WebClient()
webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", webclient)
- webclientstr = "/client"
+ webclientstr = "\n + client (ajax only)"
- #from src.server.portal import websocket
- #factory = protocol.ServerFactory()
- #websocketclient = websocket.WebSocketProtocol()
- #websocketclient.sessionhandler = PORTAL_SESSIONS
- #web_root.putChild("websocket", websocketclient)
+ if WEBSOCKET_CLIENT_ENABLED:
+ # start websocket client port for the webclient
+ from src.server.portal import websocket_client
+ from src.utils.txws import WebSocketFactory
+
+ interface = WEBSOCKET_CLIENT_INTERFACE
+ port = WEBSOCKET_CLIENT_PORT
+ ifacestr = ""
+ if interface not in ('0.0.0.0', '::'):
+ ifacestr = "-%s" % interface
+ pstring = "%s:%s" % (ifacestr, port)
+ factory = protocol.ServerFactory()
+ factory.protocol = websocket_client.WebSocketClient
+ factory.sessionhandler = PORTAL_SESSIONS
+ websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
+ websocket_service.setName('EvenniaWebSocket%s' % pstring)
+ PORTAL.services.addService(websocket_service)
+
+ webclientstr = webclientstr[:-11] + "(%s:%s)" % (WEBSOCKET_CLIENT_URL, port)
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
proxy_service = internet.TCPServer(proxyport,
@@ -274,30 +290,8 @@ if WEBSERVER_ENABLED:
interface=interface)
proxy_service.setName('EvenniaWebProxy%s' % pstring)
PORTAL.services.addService(proxy_service)
- print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport)
+ print " webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr)
-if WEBSOCKET_ENABLED:
- # websocket support is experimental!
-
- # start websocket ports for real-time web communication
-
- from src.server.portal import websocket
- from src.utils.txws import WebSocketFactory
-
- for interface in WEBSOCKET_INTERFACES:
- ifacestr = ""
- if interface not in ('0.0.0.0', '::') or len(WEBSOCKET_INTERFACES) > 1:
- ifacestr = "-%s" % interface
- for port in WEBSOCKET_PORTS:
- pstring = "%s:%s" % (ifacestr, port)
- factory = protocol.ServerFactory()
- factory.protocol = websocket.WebSocketProtocol
- factory.sessionhandler = PORTAL_SESSIONS
- websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
- websocket_service.setName('EvenniaWebSocket%s' % pstring)
- PORTAL.services.addService(websocket_service)
-
- print ' websocket%s: %s' % (ifacestr, port)
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket_client.py
similarity index 89%
rename from src/server/portal/websocket.py
rename to src/server/portal/websocket_client.py
index 7060e79249..2fd8c158f9 100644
--- a/src/server/portal/websocket.py
+++ b/src/server/portal/websocket_client.py
@@ -1,8 +1,9 @@
"""
-Websockets Protocol
+Websocket-webclient
-This implements WebSockets (http://en.wikipedia.org/wiki/WebSocket)
-by use of the txws implementation (https://github.com/MostAwesomeDude/txWS).
+This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
+by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
+used together with src/web/media/javascript/evennia_websocket_webclient.js.
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
@@ -10,13 +11,13 @@ Communication over the websocket interface is done with normal text
communication. A special case is OOB-style communication; to do this
the client must send data on the following form:
- OOB{oobfunc:[[args], {kwargs}], ...}
+ OOB{"func1":[args], "func2":[args], ...}
-where the tuple/list is sent json-encoded. The initial OOB-prefix
+where the dict is JSON encoded. The initial OOB-prefix
is used to identify this type of communication, all other data
is considered plain text (command input).
-Example of call from javascript client:
+Example of call from a javascript client:
websocket = new WeSocket("ws://localhost:8021")
var msg1 = "WebSocket Test"
@@ -33,9 +34,10 @@ from src.utils.logger import log_trace
from src.utils.utils import to_str
from src.utils.text2html import parse_html
-class WebSocketProtocol(Protocol, Session):
+
+class WebSocketClient(Protocol, Session):
"""
- This is called when the connection is first established
+ Implements the server-side of the Websocket connection.
"""
def connectionMade(self):
diff --git a/src/settings_default.py b/src/settings_default.py
index f03b3544a0..0c470fd3a7 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -58,18 +58,22 @@ UPSTREAM_IPS = ['127.0.0.1']
# with server load. Set the minimum and maximum number of threads it
# may use as (min, max) (must be > 0)
WEBSERVER_THREADPOOL_LIMITS = (1, 20)
-# Start the evennia ajax client on /webclient
-# (the webserver must also be running)
+# Start the evennia webclient. This requires the webserver to be running and
+# offers the fallback ajax-based webclient backbone for browsers not supporting
+# the websocket one.
WEBCLIENT_ENABLED = True
-# Activate Websocket support. If this is on, the default webclient will use this
-# before going for the ajax implementation
-WEBSOCKET_ENABLED = True
-# Ports to use for Websockets. If this is changed, you must also update
-# src/web/media/javascript/evennia_websocket_webclient.js to match.
-WEBSOCKET_PORTS = [8001]
+# Activate Websocket support for modern browsers. If this is on, the
+# default webclient will use this and only use the ajax version of the browser
+# is too old to support websockets. Requires WEBCLIENT_ENABLED.
+WEBSOCKET_CLIENT_ENABLED = True
+# Server-side websocket port to open for the webclient.
+WEBSOCKET_CLIENT_PORT = 8001
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
-WEBSOCKET_INTERFACES = ['0.0.0.0']
-# Activate SSH protocol (SecureShell)
+WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
+# Actual URL for webclient component to reach the websocket. The first
+# port number in the WEBSOCKET_PORTS list will be automatically appended.
+WEBSOCKET_CLIENT_URL = "ws://localhost"
+# Activate SSH protocol communication (SecureShell)
SSH_ENABLED = False
# Ports to use for SSH
SSH_PORTS = [8022]
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index 2b5e46f554..a5ba1d156a 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -2,30 +2,39 @@
Evennia websocket webclient (javascript component)
-The client is composed of several parts:
- templates/webclient.html - the main page
- webclient/views.py - the django view serving the template (based on urls.py pattern)
- src/server/portal/websockets.py - the server component talking to the client
+The client is composed of two parts:
+ src/server/portal/websocket_client.py - the portal-side component
this file - the javascript component handling dynamic content
-This implements an mud client for use with Evennia, using jQuery
-for simplicity.
-
-messages sent to the client is one of three modes:
- OOB(func,(args), func,(args), ...) - OOB command executions
- text - any other text is considered a normal text to echo
+messages sent to the client is one of two modes:
+ OOB("func1",args, "func2",args, ...) - OOB command executions, this will
+ call unique javascript functions
+ func1(args), func2(args) etc.
+ text - any other text is considered a normal text output in the main output window.
*/
-// jQuery must be imported by the calling html page before this script
-// There are plenty of help on using the jQuery library on http://jquery.com/
+// If on, allows client user to send OOB messages to server by
+// prepending with ##OOB{}, for example ##OOB{"echo":[1,2,3,4]}
+var OOB_debug = true
-// Server communications
-// Set this to the value matching settings.WEBSOCKET_PORTS
-var wsurl = "ws://localhost:8001";
+// Custom OOB functions
+// functions defined here can be called by name by the server. For
+// example the OOB{"echo":arguments} will trigger a function named
+// echo(arguments).
+
+
+function echo(message) {
+ // example echo function.
+ doShow("out", "ECHO return: " + message) }
+
+
+
+
+// Webclient code
function webclient_init(){
- // initializing the client once the html page has loaded
+ // called when client is just initializing
websocket = new WebSocket(wsurl);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
@@ -34,91 +43,100 @@ function webclient_init(){
}
function onOpen(evt) {
- // client is just connecting
+ // called when client is first connecting
$("#connecting").remove(); // remove the "connecting ..." message
- msg_display("sys", "Using websockets - connected to " + wsurl + ".")
+ doShow("sys", "Using websockets - connected to " + wsurl + ".")
setTimeout(function () {
- $("#playercount").fadeOut('slow', webclient_set_sizes);
+ $("#playercount").fadeOut('slow', doSetSizes);
}, 10000);
}
function onClose(evt) {
- // client is closing
+ // called when client is closing
CLIENT_HASH = 0;
alert("Mud client connection was closed cleanly.");
}
function onMessage(evt) {
- // outgoing message from server
+ // called when the Evennia is sending data to client
var inmsg = evt.data
if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
// dynamically call oob methods, if available
try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB }
catch(err) {
// not JSON packed - a normal text
- msg_display('out', err + " " + inmsg);
+ doShow('out', err + " " + inmsg);
return;
}
for (var ind in oobarray) {
try { window[oobarray[ind][0]](oobarray[ind][1]) }
- catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") }
+ catch(err) { doShow("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") }
}
}
else {
// normal message
- msg_display('out', inmsg); }
+ doShow('out', inmsg); }
}
function onError(evt) {
- // client error message
- msg_display('err', "Error: Server returned an error. Try reloading the page.");
+ // called on a server error
+ doShow('err', "Error: Server returned an error. Try reloading the page.");
}
function doSend(){
- // sending data from client to server
+ // relays data from client to Evennia.
+ // If OOB_debug is set, allows OOB test data on the
+ // form ##OOB{func:args}
outmsg = $("#inputfield").val();
history_add(outmsg);
HISTORY_POS = 0;
$('#inputform')[0].reset(); // clear input field
- if (outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
+ if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
// test OOB messaging
doOOB(JSON.parse(outmsg.slice(5))); }
else {
+ // normal output
websocket.send(outmsg); }
}
function doOOB(oobdict){
- // Handle OOB communication from client side
- // Takes a dict on form {funcname:[args], funcname: [args], ... ]
- msg_display("out", "into doOOB: " + oobdict)
- msg_display("out", "stringify: " + JSON.stringify(oobdict))
+ // Send OOB data from client to Evennia.
+ // Takes input on form {funcname:[args], funcname: [args], ... }
var oobmsg = JSON.stringify(oobdict);
websocket.send("OOB" + oobmsg);
}
-
-//
-// OOB functions
-//
-
-function echo(message) {
- msg_display("out", "ECHO return: " + message) }
-
-//
-// Display messages
-
-function msg_display(type, msg){
- // Add a div to the message window.
+function doShow(type, msg){
+ // Add msg to the main output window.
// type gives the class of div to use.
+ // The default types are
+ // "out" (normal output) or "err" (red error message)
$("#messagewindow").append(
"
"+ msg +"
");
// scroll message window to bottom
$('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight});
}
-// Input history mechanism
+
+function doSetSizes() {
+ // Sets the size of the message window
+ var win_h = $(document).height();
+ //var win_w = $('#wrapper').width();
+ var inp_h = $('#inputform').outerHeight(true);
+ //var inp_w = $('#inputsend').outerWidth(true);
+
+ $("#messagewindow").css({'height': win_h - inp_h - 1});
+ //$("#inputfield").css({'width': win_w - inp_w - 20});
+}
+
+
+//
+// Input code
+//
+
+// Input history
var HISTORY_MAX_LENGTH = 21
var HISTORY = new Array();
@@ -223,6 +241,8 @@ $.fn.appendCaret = function() {
});
};
+// Input jQuery callbacks
+
$(document).keydown( function(event) {
// Get the pressed key (normalized by jQuery)
var code = event.which,
@@ -233,7 +253,7 @@ $(document).keydown( function(event) {
// Special keys recognized by client
- //msg_display("out", "key code pressed: " + code); // debug
+ //doShow("out", "key code pressed: " + code); // debug
if (code == 13) { // Enter Key
doSend();
@@ -252,36 +272,24 @@ $(document).keydown( function(event) {
// handler to avoid double-clicks until the ajax request finishes
//$("#inputsend").one("click", webclient_input)
-function webclient_set_sizes() {
- // Sets the size of the message window
- var win_h = $(document).height();
- //var win_w = $('#wrapper').width();
- var inp_h = $('#inputform').outerHeight(true);
- //var inp_w = $('#inputsend').outerWidth(true);
+// Callback function - called when the browser window resizes
+$(window).resize(doSetSizes);
- $("#messagewindow").css({'height': win_h - inp_h - 1});
- //$("#inputfield").css({'width': win_w - inp_w - 20});
-}
-
-
-// Callback function - called when page has finished loading (gets things going)
+// Callback function - called when page is closed or moved away from.
+//$(window).bind("beforeunload", webclient_close);
+//
+// Callback function - called when page has finished loading (kicks the client into gear)
$(document).ready(function(){
// remove the "no javascript" warning, since we obviously have javascript
$('#noscript').remove();
// set sizes of elements and reposition them
- webclient_set_sizes();
+ doSetSizes();
// a small timeout to stop 'loading' indicator in Chrome
setTimeout(function () {
webclient_init();
}, 500);
// set an idle timer to avoid proxy servers to time out on us (every 3 minutes)
setInterval(function() {
- webclient_input("idle", true);
+ websocket.send("idle");
}, 60000*3);
});
-
-// Callback function - called when the browser window resizes
-$(window).resize(webclient_set_sizes);
-
-// Callback function - called when page is closed or moved away from.
-//$(window).bind("beforeunload", webclient_close);
diff --git a/src/web/templates/prosimii/webclient.html b/src/web/templates/prosimii/webclient.html
index ba7769af23..f36839e708 100644
--- a/src/web/templates/prosimii/webclient.html
+++ b/src/web/templates/prosimii/webclient.html
@@ -19,9 +19,10 @@
")}
else {
-
+
document.write("\")}
{% else %}
diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py
index 421bf9587a..b901e856e3 100644
--- a/src/web/utils/general_context.py
+++ b/src/web/utils/general_context.py
@@ -6,7 +6,6 @@
# tuple.
#
-from django.db import models
from django.conf import settings
from src.utils.utils import get_evennia_version
@@ -30,6 +29,9 @@ WEBSITE = ['Flatpages', 'News', 'Sites']
# The main context processor function
+WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
+WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED
+WSURL = "%s:%s" % (settings.WEBSOCKET_CLIENT_URL, settings.WEBSOCKET_CLIENT_PORT)
def general_context(request):
"""
@@ -44,6 +46,7 @@ def general_context(request):
'evennia_setupapps': GAME_SETUP,
'evennia_connectapps': CONNECTIONS,
'evennia_websiteapps':WEBSITE,
- "webclient_enabled" : settings.WEBCLIENT_ENABLED,
- "websocket_enabled" : settings.WEBSOCKET_ENABLED
+ "webclient_enabled" : WEBCLIENT_ENABLED,
+ "websocket_enabled" : WEBSOCKET_CLIENT_ENABLED,
+ "websocket_url" : WSURL
}
From 9ba212c2648260d698c6d58c04b6ababa458bc30 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Fri, 27 Jun 2014 01:13:48 +0200
Subject: [PATCH 08/25] Working on the OOB system, somewhat unstable at the
moment.
---
src/server/oob_cmds.py | 175 ++++++++++
src/server/oob_msdp.py | 317 ------------------
src/server/oobhandler.py | 87 ++++-
src/server/sessionhandler.py | 10 +-
src/settings_default.py | 2 +-
.../javascript/evennia_websocket_webclient.js | 3 +-
6 files changed, 252 insertions(+), 342 deletions(-)
create mode 100644 src/server/oob_cmds.py
delete mode 100644 src/server/oob_msdp.py
diff --git a/src/server/oob_cmds.py b/src/server/oob_cmds.py
new file mode 100644
index 0000000000..88236104e0
--- /dev/null
+++ b/src/server/oob_cmds.py
@@ -0,0 +1,175 @@
+"""
+Out-of-band default plugin commands available for OOB handler.
+
+This module implements commands as defined by the MSDP standard
+(http://tintin.sourceforge.net/msdp/), but is independent of the
+actual transfer protocol (webclient, MSDP, GMCP etc).
+
+This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
+(not classes) defined globally in this module will be made available
+to the oob mechanism.
+
+oob functions have the following call signature:
+ function(oobhandler, session, *args, **kwargs)
+
+where oobhandler is a back-reference to the central OOB_HANDLER
+instance and session is the active session to get return data.
+
+The function names are not case-sensitive (this allows for names
+like "LIST" which would otherwise collide with Python builtins).
+
+A function named _OOB_ERROR will retrieve error strings if it is
+defined. It will get the error message as its 3rd argument.
+"""
+
+from django.conf import settings
+_GA = object.__getattribute__
+_SA = object.__setattr__
+_NA_REPORT = lambda o: (None, "N/A")
+_NA_SEND = lambda o: "N/A"
+
+#------------------------------------------------------------
+# All OOB commands must be on the form
+# cmdname(oobhandler, session, *args, **kwargs)
+#------------------------------------------------------------
+
+def _OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
+ """
+ A function with this name is special and is called by the oobhandler when an error
+ occurs already at the execution stage (such as the oob function
+ not being recognized or having the wrong args etc).
+ """
+ session.msg(oob=("send", {"ERROR": errmsg}))
+
+
+def ECHO(oobhandler, session, *args, **kwargs):
+ "Test/debug function, simply returning the args and kwargs"
+ session.msg(oob=("echo", args, kwargs))
+
+
+def SEND(oobhandler, session, *args, **kwargs):
+ """
+ This function directly returns the value of the given variable to the
+ session.
+ """
+ print "In SEND:", oobhandler, session, args
+ obj = session.get_puppet_or_player()
+ ret = {}
+ if obj:
+ for name in (a.upper() for a in args if a):
+ try:
+ value = OOB_SENDABLE.get(name, _NA_SEND)(obj)
+ ret[name] = value
+ except Exception, e:
+ ret[name] = str(e)
+ # return result
+ session.msg(oob=("send", ret))
+
+
+def REPORT(oobhandler, session, *args, **kwargs):
+ """
+ This creates a tracker instance to track the data given in *args.
+
+ Note that the data name is assumed to be a field is it starts with db_*
+ and an Attribute otherwise.
+
+ "Example of tracking changes to the db_key field and the desc" Attribite:
+ REPORT(oobhandler, session, "CHARACTER_NAME", )
+ """
+ obj = session.get_puppet_or_player()
+ if obj:
+ for name in (a.upper() for a in args if a):
+ typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)(obj)
+ if typ == "field":
+ oobhandler.track_field(obj, session.sessid, name)
+ elif typ == "attribute":
+ oobhandler.track_attribute(obj, session.sessid, name)
+
+
+def UNREPORT(oobhandler, session, vartype="prop", *args, **kwargs):
+ """
+ This removes tracking for the given data given in *args.
+ """
+ obj = session.get_puppet_or_player()
+ if obj:
+ for name in (a.upper() for a in args if a):
+ typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)
+ if typ == "field":
+ oobhandler.untrack_field(obj, session.sessid, name)
+ else: # assume attribute
+ oobhandler.untrack_attribute(obj, session.sessid, name)
+
+
+def LIST(oobhandler, session, mode, *args, **kwargs):
+ """
+ List available properties. Mode is the type of information
+ desired:
+ "COMMANDS" Request an array of commands supported
+ by the server.
+ "LISTS" Request an array of lists supported
+ by the server.
+ "CONFIGURABLE_VARIABLES" Request an array of variables the client
+ can configure.
+ "REPORTABLE_VARIABLES" Request an array of variables the server
+ will report.
+ "REPORTED_VARIABLES" Request an array of variables currently
+ being reported.
+ "SENDABLE_VARIABLES" Request an array of variables the server
+ will send.
+ """
+ mode = mode.upper()
+ if mode == "COMMANDS":
+ session.msg(oob=("list", ("COMMANDS",
+ "LIST",
+ "REPORT",
+ "UNREPORT",
+ # "RESET",
+ "SEND")))
+ elif mode == "LISTS":
+ session.msg(oob=("list", ("LISTS",
+ "REPORTABLE_VARIABLES",
+ "REPORTED_VARIABLES",
+ # "CONFIGURABLE_VARIABLES",
+ "SENDABLE_VARIABLES")))
+ elif mode == "REPORTABLE_VARIABLES":
+ session.msg(oob=("list", ("REPORTABLE_VARIABLES",) +
+ tuple(key for key in OOB_REPORTABLE.keys())))
+ elif mode == "REPORTED_VARIABLES":
+ session.msg(oob=("list", ("REPORTED_VARIABLES",) +
+ tuple(oobhandler.get_all_tracked(session))))
+ elif mode == "SENDABLE_VARIABLES":
+ session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
+ tuple(key for key in OOB_REPORTABLE.keys())))
+ #elif mode == "CONFIGURABLE_VARIABLES":
+ # pass
+ else:
+ session.msg(oob=("list", ("unsupported mode",)))
+
+
+# Mapping for how to retrieve each property name.
+# Each entry should point to a callable that gets the interesting object as
+# input and returns the relevant value.
+
+# MSDP recommends the following standard name mappings for general compliance:
+# "CHARACTER_NAME", "SERVER_ID", "SERVER_TIME", "AFFECTS", "ALIGNMENT", "EXPERIENCE", "EXPERIENCE_MAX", "EXPERIENCE_TNL",
+# "HEALTH", "HEALTH_MAX", "LEVEL", "RACE", "CLASS", "MANA", "MANA_MAX", "WIMPY", "PRACTICE", "MONEY", "MOVEMENT",
+# "MOVEMENT_MAX", "HITROLL", "DAMROLL", "AC", "STR", "INT", "WIS", "DEX", "CON", "OPPONENT_HEALTH", "OPPONENT_HEALTH_MAX",
+# "OPPONENT_LEVEL", "OPPONENT_NAME", "AREA_NAME", "ROOM_EXITS", "ROOM_VNUM", "ROOM_NAME", "WORLD_TIME", "CLIENT_ID",
+# "CLIENT_VERSION", "PLUGIN_ID", "ANSI_COLORS", "XTERM_256_COLORS", "UTF_8", "SOUND", "MXP", "BUTTON_1", "BUTTON_2",
+# "BUTTON_3", "BUTTON_4", "BUTTON_5", "GAUGE_1", "GAUGE_2","GAUGE_3", "GAUGE_4", "GAUGE_5"
+
+OOB_SENDABLE = {
+ "CHARACTER_NAME": lambda o: o.key,
+ "SERVER_ID": lambda o: settings.SERVERNAME,
+ "ROOM_NAME": lambda o: o.db_location.key,
+ "ANSI_COLORS": lambda o: True,
+ "XTERM_256_COLORS": lambda o: True,
+ "UTF_8": lambda o: True
+ }
+
+# mapping for which properties may be tracked. Each callable should return a tuple (type, value) where
+# the type is one of "field" or "attribute" depending on what is being tracked.
+OOB_REPORTABLE = {
+ "CHARACTER_NAME": lambda o: ("field", o.key),
+ "ROOM_NAME": lambda o: ("attribute", o.db_location.key)
+ }
diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py
deleted file mode 100644
index 6b474fde79..0000000000
--- a/src/server/oob_msdp.py
+++ /dev/null
@@ -1,317 +0,0 @@
-"""
-Out-of-band default plugin commands available for OOB handler. This
-follows the standards defined by the MSDP out-of-band protocol
-(http://tintin.sourceforge.net/msdp/)
-
-This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
-(not classes) defined globally in this module will be made available
-to the oob mechanism.
-
- function execution - the oob protocol can execute a function directly on
- the server. The available functions must be defined
- as global functions via settings.OOB_PLUGIN_MODULES.
- repeat func execution - the oob protocol can request a given function be
- executed repeatedly at a regular interval. This
- uses an internal script pool.
- tracking - the oob protocol can request Evennia to track changes to
- fields on objects, as well as changes in Attributes. This is
- done by dynamically adding tracker-objects on entities. The
- behaviour of those objects can be customized via
- settings.OOB_PLUGIN_MODULES.
-
-What goes into the OOB_PLUGIN_MODULES is a list of modules with input
-for the OOB system.
-
-oob functions have the following call signature:
- function(caller, session, *args, **kwargs)
-
-oob trackers should build upon the OOBTracker class in this module
- module and implement a minimum of the same functionality.
-
-a global function oob_error will be used as optional error management.
-"""
-from django.conf import settings
-from src.utils.utils import to_str
-_GA = object.__getattribute__
-_SA = object.__setattr__
-_NA = lambda o: (None, "N/A") # not implemented
-
-# default properties defined by the MSDP protocol. These are
-# used by the SEND oob function below. Each entry should point
-# to a function that takes the relevant object as input and
-# returns the data it is responsible for. Most of these
-# are commented out, but kept for reference for each
-# game to implement.
-
-OOB_SENDABLE = {
- ## General
- "CHARACTER_NAME": lambda o: ("db_key", o.key),
- "SERVER_ID": lambda o: ("settings.SERVERNAME", settings.SERVERNAME),
- #"SERVER_TIME": _NA,
- ## Character
- #"AFFECTS": _NA,
- #"ALIGNMENT": _NA,
- #"EXPERIENCE": _NA,
- #"EXPERIENCE_MAX": _NA,
- #"EXPERIENCE_TNL": _NA,
- #"HEALTH": _NA,
- #"HEALTH_MAX": _NA,
- #"LEVEL": _NA,
- #"RACE": _NA,
- #"CLASS": _NA,
- #"MANA": _NA,
- #"MANA_MAX": _NA,
- #"WIMPY": _NA,
- #"PRACTICE": _NA,
- #"MONEY": _NA,
- #"MOVEMENT": _NA,
- #"MOVEMENT_MAX": _NA,
- #"HITROLL": _NA,
- #"DAMROLL": _NA,
- #"AC": _NA,
- #"STR": _NA,
- #"INT": _NA,
- #"WIS": _NA,
- #"DEX": _NA,
- #"CON": _NA,
- ## Combat
- #"OPPONENT_HEALTH": _NA,
- #"OPPONENT_HEALTH_MAX": _NA,
- #"OPPONENT_LEVEL": _NA,
- #"OPPONENT_NAME": _NA,
- ## World
- #"AREA_NAME": _NA,
- #"ROOM_EXITS": _NA,
- #"ROOM_VNUM": _NA,
- "ROOM_NAME": lambda o: ("db_location", o.db_location.key),
- #"WORLD_TIME": _NA,
- ## Configurable variables
- #"CLIENT_ID": _NA,
- #"CLIENT_VERSION": _NA,
- #"PLUGIN_ID": _NA,
- #"ANSI_COLORS": _NA,
- #"XTERM_256_COLORS": _NA,
- #"UTF_8": _NA,
- #"SOUND": _NA,
- #"MXP": _NA,
- ## GUI variables
- #"BUTTON_1": _NA,
- #"BUTTON_2": _NA,
- #"BUTTON_3": _NA,
- #"BUTTON_4": _NA,
- #"BUTTON_5": _NA,
- #"GAUGE_1": _NA,
- #"GAUGE_2": _NA,
- #"GAUGE_3": _NA,
- #"GAUGE_4": _NA,
- #"GAUGE_5": _NA
- }
-# mapping for which properties may be tracked
-OOB_REPORTABLE = OOB_SENDABLE
-
-
-#------------------------------------------------------------
-# Tracker classes
-#
-# Trackers are added to a given object's trackerhandler and
-# reports back changes when they happen. They are managed using
-# the oobhandler's track/untrack mechanism
-#------------------------------------------------------------
-
-class TrackerBase(object):
- """
- Base class for OOB Tracker objects.
- """
- def __init__(self, oobhandler, *args, **kwargs):
- self.oobhandler = oobhandler
-
- def update(self, *args, **kwargs):
- "Called by tracked objects"
- pass
-
- def at_remove(self, *args, **kwargs):
- "Called when tracker is removed"
- pass
-
-
-# Tracker objects stored on objects when using the MSDP report command
-
-class ReportFieldTracker(TrackerBase):
- """
- Tracker that passively sends data to a stored sessid whenever
- a named database field changes. The TrackerHandler calls this with
- the correct arguments.
- """
- def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs):
- """
- name - name of entity to track, such as "db_key"
- sessid - sessid of session to report to
- """
- self.oobhandler = oobhandler
- self.fieldname = fieldname
- self.sessid = sessid
-
- def update(self, new_value, *args, **kwargs):
- "Called by cache when updating the tracked entitiy"
- # use oobhandler to relay data
- try:
- # we must never relay objects across the amp, only text data.
- new_value = new_value.key
- except AttributeError:
- new_value = to_str(new_value, force_string=True)
- # this is a wrapper call for sending oob data back to session
- self.oobhandler.msg(self.sessid, "report", self.fieldname,
- new_value, *args, **kwargs)
-
-
-class ReportAttributeTracker(TrackerBase):
- """
- Tracker that passively sends data to a stored sessid whenever
- the Attribute updates. Since the field here is always "db_key",
- we instead store the name of the attribute to return.
- """
- def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs):
- """
- attrname - name of attribute to track
- sessid - sessid of session to report to
- """
- self.oobhandler = oobhandler
- self.attrname = attrname
- self.sessid = sessid
-
- def update(self, new_value, *args, **kwargs):
- "Called by cache when attribute's db_value field updates"
- try:
- new_value = new_value.dbobj
- except AttributeError:
- new_value = to_str(new_value, force_string=True)
- # this is a wrapper call for sending oob data back to session
- self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
-
-
-#------------------------------------------------------------
-# OOB commands
-# This defines which internal server commands the OOB handler
-# makes available to the client. These commands are called
-# automatically by the OOB mechanism by triggering the
-# oobhandlers's execute_cmd method with the cmdname and
-# eventual args/kwargs. All functions defined globally in this
-# module will be made available to call by the oobhandler. Use
-# _funcname if you want to exclude one. To allow for python-names
-# like "list" here, these properties are case-insensitive.
-#
-# All OOB commands must be on the form
-# cmdname(oobhandler, session, *args, **kwargs)
-#------------------------------------------------------------
-
-def oob_error(oobhandler, session, errmsg, *args, **kwargs):
- """
- This is a special function called by the oobhandler when an error
- occurs already at the execution stage (such as the oob function
- not being recognized or having the wrong args etc).
- """
- session.msg(oob=("send", {"ERROR": errmsg}))
-
-def LIST(oobhandler, session, mode, *args, **kwargs):
- """
- List available properties. Mode is the type of information
- desired:
- "COMMANDS" Request an array of commands supported
- by the server.
- "LISTS" Request an array of lists supported
- by the server.
- "CONFIGURABLE_VARIABLES" Request an array of variables the client
- can configure.
- "REPORTABLE_VARIABLES" Request an array of variables the server
- will report.
- "REPORTED_VARIABLES" Request an array of variables currently
- being reported.
- "SENDABLE_VARIABLES" Request an array of variables the server
- will send.
- """
- mode = mode.upper()
- # the first return argument is treated by the msdp protocol as the
- # name of the msdp array to return
- if mode == "COMMANDS":
- session.msg(oob=("list", ("COMMANDS",
- "LIST",
- "REPORT",
- "UNREPORT",
- # "RESET",
- "SEND")))
- elif mode == "LISTS":
- session.msg(oob=("list", ("LISTS",
- "REPORTABLE_VARIABLES",
- "REPORTED_VARIABLES",
- # "CONFIGURABLE_VARIABLES",
- "SENDABLE_VARIABLES")))
- elif mode == "REPORTABLE_VARIABLES":
- session.msg(oob=("list", ("REPORTABLE_VARIABLES",) +
- tuple(key for key in OOB_REPORTABLE.keys())))
- elif mode == "REPORTED_VARIABLES":
- session.msg(oob=("list", ("REPORTED_VARIABLES",) +
- tuple(oobhandler.get_all_tracked(session))))
- elif mode == "SENDABLE_VARIABLES":
- session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
- tuple(key for key in OOB_REPORTABLE.keys())))
- #elif mode == "CONFIGURABLE_VARIABLES":
- # pass
- else:
- session.msg(oob=("list", ("unsupported mode",)))
-
-
-def SEND(oobhandler, session, *args, **kwargs):
- """
- This function directly returns the value of the given variable to the
- session. vartype can be one of
- """
- obj = session.get_puppet_or_player()
- ret = {}
- if obj:
- for name in (a.upper() for a in args if a):
- try:
- key, value = OOB_SENDABLE.get(name, _NA)(obj)
- ret[name] = value
- except Exception, e:
- ret[name] = str(e)
- # return result
- session.msg(oob=("send", ret))
-
-
-def REPORT(oobhandler, session, *args, **kwargs):
- """
- This creates a tracker instance to track the data given in *args.
- vartype is one of "prop" (database fields) or "attr" (attributes)
- """
- obj = session.get_puppet_or_player()
- if obj:
- for name in (a.upper() for a in args if a):
- key, val = OOB_REPORTABLE.get(name, _NA)(obj)
- if key:
- if key.startswith("db_"):
- oobhandler.track_field(obj, session.sessid,
- key, ReportFieldTracker)
- else: # assume attribute
- oobhandler.track_attribute(obj, session.sessid,
- key, ReportAttributeTracker)
-
-
-def UNREPORT(oobhandler, session, vartype="prop", *args, **kwargs):
- """
- This removes tracking for the given data given in *args.
- vartype is one of of "prop" or "attr".
- """
- obj = session.get_puppet_or_player()
- if obj:
- for name in (a.upper() for a in args if a):
- key, val = OOB_REPORTABLE.get(name, _NA)
- if key:
- if key.startswith("db_"):
- oobhandler.untrack_field(obj, session.sessid, key)
- else: # assume attribute
- oobhandler.untrack_attribute(obj, session.sessid, key)
-
-def ECHO(oobhandler, session, *args, **kwargs):
- "Test function, returning the args, kwargs"
- args = ["Return echo:"] + list(args)
- session.msg(oob=("echo", args, kwargs))
diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py
index fea7a5642a..aeacfe3c04 100644
--- a/src/server/oobhandler.py
+++ b/src/server/oobhandler.py
@@ -45,7 +45,7 @@ from src.server.sessionhandler import SESSIONS
from src.scripts.tickerhandler import Ticker, TickerPool, TickerHandler
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
from src.utils import logger
-from src.utils.utils import all_from_module, make_iter
+from src.utils.utils import all_from_module, make_iter, to_str
_SA = object.__setattr__
_GA = object.__getattribute__
@@ -55,9 +55,9 @@ _DA = object.__delattr__
_OOB_FUNCS = {}
for mod in make_iter(settings.OOB_PLUGIN_MODULES):
_OOB_FUNCS.update(dict((key.lower(), func) for key, func in all_from_module(mod).items() if isfunction(func)))
-# get custom error method or use the default
-_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
+# get custom error method or use the default
+_OOB_ERROR = _OOB_FUNCS.get("_OOB_ERROR", None)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
@@ -131,11 +131,12 @@ class TrackerHandler(object):
logger.log_trace()
-# Tracker loaded by the TrackerHandler
+# On-object Trackers to load with TrackerHandler
class TrackerBase(object):
"""
- Base class for OOB Tracker objects.
+ Base class for OOB Tracker objects. Inherit from this
+ to define custom trackers.
"""
def __init__(self, *args, **kwargs):
pass
@@ -148,6 +149,61 @@ class TrackerBase(object):
"Called when tracker is removed"
pass
+
+class ReportFieldTracker(TrackerBase):
+ """
+ Tracker that passively sends data to a stored sessid whenever
+ a named database field changes. The TrackerHandler calls this with
+ the correct arguments.
+ """
+ def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs):
+ """
+ name - name of entity to track, such as "db_key"
+ sessid - sessid of session to report to
+ """
+ self.oobhandler = oobhandler
+ self.fieldname = fieldname
+ self.sessid = sessid
+
+ def update(self, new_value, *args, **kwargs):
+ "Called by cache when updating the tracked entitiy"
+ # use oobhandler to relay data
+ try:
+ # we must never relay objects across the amp, only text data.
+ new_value = new_value.key
+ except AttributeError:
+ new_value = to_str(new_value, force_string=True)
+ # this is a wrapper call for sending oob data back to session
+ self.oobhandler.msg(self.sessid, "report", self.fieldname,
+ new_value, *args, **kwargs)
+
+
+class ReportAttributeTracker(TrackerBase):
+ """
+ Tracker that passively sends data to a stored sessid whenever
+ the Attribute updates. Since the field here is always "db_key",
+ we instead store the name of the attribute to return.
+ """
+ def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs):
+ """
+ attrname - name of attribute to track
+ sessid - sessid of session to report to
+ """
+ self.oobhandler = oobhandler
+ self.attrname = attrname
+ self.sessid = sessid
+
+ def update(self, new_value, *args, **kwargs):
+ "Called by cache when attribute's db_value field updates"
+ try:
+ new_value = new_value.dbobj
+ except AttributeError:
+ new_value = to_str(new_value, force_string=True)
+ # this is a wrapper call for sending oob data back to session
+ self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
+
+
+
# Ticker of auto-updating objects
class OOBTicker(Ticker):
@@ -273,7 +329,7 @@ class OOBHandler(object):
sessid = session.sessid
return [key[2].lstrip("db_") for key in self.oob_tracker_storage.keys() if key[1] == sessid]
- def track_field(self, obj, sessid, field_name, trackerclass):
+ def track_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
Shortcut wrapper method for specifically tracking a database field.
Takes the tracker class as argument.
@@ -289,7 +345,7 @@ class OOBHandler(object):
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._untrack(obj, sessid, field_name)
- def track_attribute(self, obj, sessid, attr_name, trackerclass):
+ def track_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
Shortcut wrapper method for specifically tracking the changes of an
Attribute on an object. Will create a tracker on the Attribute
@@ -332,12 +388,6 @@ class OOBHandler(object):
"""
self.tickerhandler.remove(self, obj, interval)
- def msg(self, sessid, funcname, *args, **kwargs):
- "Shortcut to relay oob data back to portal. Used by oob functions."
- session = self.sessionhandler.session_from_sessid(sessid)
- #print "oobhandler msg:", sessid, session, funcname, args, kwargs
- if session:
- session.msg(oob=(funcname, args, kwargs))
# access method - called from session.msg()
@@ -347,7 +397,7 @@ class OOBHandler(object):
using *args and **kwargs
"""
try:
- print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
+ #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc(self, session, *args, **kwargs)
except KeyError,e:
@@ -364,5 +414,14 @@ class OOBHandler(object):
else:
logger.log_trace(errmsg)
raise Exception(errmsg)
+
+ def msg(self, sessid, funcname, *args, **kwargs):
+ "Shortcut to force-send an OOB message through the oobhandler to a session"
+ session = self.sessionhandler.session_from_sessid(sessid)
+ #print "oobhandler msg:", sessid, session, funcname, args, kwargs
+ if session:
+ session.msg(oob=(funcname, args, kwargs))
+
+
# access object
OOB_HANDLER = OOBHandler()
diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py
index ec69d35c6e..b911226095 100644
--- a/src/server/sessionhandler.py
+++ b/src/server/sessionhandler.py
@@ -103,15 +103,9 @@ class SessionHandler(object):
def oobstruct_parser(self, oobstruct):
"""
Helper method for each session to use to parse oob structures
- (The 'oob' kwarg of the msg() method)
- ((cmdname, (args), {}), ...)
-
-
- Allowed oob structures are:
- allowed oob structures are
-
-
+ (The 'oob' kwarg of the msg() method).
+ Allowed input oob structures are:
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))
diff --git a/src/settings_default.py b/src/settings_default.py
index 0c470fd3a7..9a6d466a52 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -230,7 +230,7 @@ LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)
# Module holding OOB (Out of Band) hook objects. This allows for customization
# and expansion of which hooks OOB protocols are allowed to call on the server
# protocols for attaching tracker hooks for when various object field change
-OOB_PLUGIN_MODULES = ["src.server.oob_msdp"]
+OOB_PLUGIN_MODULES = ["src.server.oob_cmds"]
######################################################################
# Default command sets
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index a5ba1d156a..b604fc92fc 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -29,8 +29,6 @@ function echo(message) {
doShow("out", "ECHO return: " + message) }
-
-
// Webclient code
function webclient_init(){
@@ -95,6 +93,7 @@ function doSend(){
if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
// test OOB messaging
+ doShow("out", "OOB input: " + outmsg.slice(5))
doOOB(JSON.parse(outmsg.slice(5))); }
else {
// normal output
From c60a5fdea1c60128e3b7c83b21354526e22350f7 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sat, 28 Jun 2014 17:38:21 +0200
Subject: [PATCH 09/25] Fixed and refactored OOB system and tested with new
websocket client
---
src/objects/models.py | 4 +-
src/server/caches.py | 44 ++++----
src/server/oob_cmds.py | 102 +++++++++++++-----
src/server/oobhandler.py | 101 +++++++++--------
src/server/portal/websocket_client.py | 6 +-
src/server/server.py | 4 +-
src/server/serversession.py | 1 +
src/server/sessionhandler.py | 7 +-
src/typeclasses/models.py | 9 +-
.../javascript/evennia_websocket_webclient.js | 59 ++++++++--
10 files changed, 209 insertions(+), 128 deletions(-)
diff --git a/src/objects/models.py b/src/objects/models.py
index 409c6aad0f..7ad13c661b 100644
--- a/src/objects/models.py
+++ b/src/objects/models.py
@@ -144,9 +144,9 @@ class ObjectDB(TypedObject):
# make sure to sync the contents cache when initializing
#_GA(self, "contents_update")()
- def _at_db_player_presave(self):
+ def _at_db_player_postsave(self):
"""
- This hook is called automatically just before the player field is saved.
+ This hook is called automatically after the player field is saved.
"""
# we need to re-cache this for superusers to bypass.
self.locks.cache_lock_bypass(self)
diff --git a/src/server/caches.py b/src/server/caches.py
index 3405b0aa53..c55d50df3d 100644
--- a/src/server/caches.py
+++ b/src/server/caches.py
@@ -80,28 +80,28 @@ def hashid(obj, suffix=""):
#------------------------------------------------------------
# callback to field pre_save signal (connected in src.server.server)
-def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
- """
- Called at the beginning of the field save operation. The save method
- must be called with the update_fields keyword in order to be most efficient.
- This method should NOT save; rather it is the save() that triggers this
- function. Its main purpose is to allow to plug-in a save handler and oob
- handlers.
- """
- if raw:
- return
- if update_fields:
- # this is a list of strings at this point. We want field objects
- update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
- else:
- # meta.fields are already field objects; get them all
- update_fields = _GA(_GA(instance, "_meta"), "fields")
- for field in update_fields:
- fieldname = field.name
- handlername = "_at_%s_presave" % fieldname
- handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
- if callable(handler):
- handler()
+#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
+# """
+# Called at the beginning of the field save operation. The save method
+# must be called with the update_fields keyword in order to be most efficient.
+# This method should NOT save; rather it is the save() that triggers this
+# function. Its main purpose is to allow to plug-in a save handler and oob
+# handlers.
+# """
+# if raw:
+# return
+# if update_fields:
+# # this is a list of strings at this point. We want field objects
+# update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
+# else:
+# # meta.fields are already field objects; get them all
+# update_fields = _GA(_GA(instance, "_meta"), "fields")
+# for field in update_fields:
+# fieldname = field.name
+# handlername = "_at_%s_presave" % fieldname
+# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
+# if callable(handler):
+# handler()
def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
diff --git a/src/server/oob_cmds.py b/src/server/oob_cmds.py
index 88236104e0..42a762c3bf 100644
--- a/src/server/oob_cmds.py
+++ b/src/server/oob_cmds.py
@@ -18,14 +18,19 @@ instance and session is the active session to get return data.
The function names are not case-sensitive (this allows for names
like "LIST" which would otherwise collide with Python builtins).
-A function named _OOB_ERROR will retrieve error strings if it is
+A function named OOB_ERROR will retrieve error strings if it is
defined. It will get the error message as its 3rd argument.
+
+Data is usually returned via
+ session.msg(oob=(cmdname, (args,), {kwargs}))
+Note that args, kwargs must be iterable/dict, non-iterables will
+be interpreted as a new command name.
+
"""
from django.conf import settings
_GA = object.__getattribute__
_SA = object.__setattr__
-_NA_REPORT = lambda o: (None, "N/A")
_NA_SEND = lambda o: "N/A"
#------------------------------------------------------------
@@ -33,26 +38,25 @@ _NA_SEND = lambda o: "N/A"
# cmdname(oobhandler, session, *args, **kwargs)
#------------------------------------------------------------
-def _OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
+def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
"""
A function with this name is special and is called by the oobhandler when an error
occurs already at the execution stage (such as the oob function
not being recognized or having the wrong args etc).
"""
- session.msg(oob=("send", {"ERROR": errmsg}))
+ session.msg(oob=("err", ("ERROR " + errmsg,)))
def ECHO(oobhandler, session, *args, **kwargs):
"Test/debug function, simply returning the args and kwargs"
session.msg(oob=("echo", args, kwargs))
-
+##OOB{"SEND":"CHARACTER_NAME"}
def SEND(oobhandler, session, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session.
"""
- print "In SEND:", oobhandler, session, args
obj = session.get_puppet_or_player()
ret = {}
if obj:
@@ -62,14 +66,18 @@ def SEND(oobhandler, session, *args, **kwargs):
ret[name] = value
except Exception, e:
ret[name] = str(e)
- # return result
- session.msg(oob=("send", ret))
-
+ session.msg(oob=("send", ret))
+ else:
+ session.msg(oob=("err", ("You must log in first.",)))
+##OOB{"REPORT":"TEST"}
def REPORT(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
+ The tracker will return with a oob structure
+ oob={"report":["attrfieldname", (args,), {kwargs}}
+
Note that the data name is assumed to be a field is it starts with db_*
and an Attribute otherwise.
@@ -79,27 +87,37 @@ def REPORT(oobhandler, session, *args, **kwargs):
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
- typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)(obj)
- if typ == "field":
- oobhandler.track_field(obj, session.sessid, name)
- elif typ == "attribute":
- oobhandler.track_attribute(obj, session.sessid, name)
+ trackname = OOB_REPORTABLE.get(name, None)
+ if not trackname:
+ session.msg(oob=("err", ("No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % trackname,)))
+ elif trackname.startswith("db_"):
+ oobhandler.track_field(obj, session.sessid, trackname)
+ else:
+ oobhandler.track_attribute(obj, session.sessid, trackname)
+ else:
+ session.msg(oob=("err", ("You must log in first.",)))
-def UNREPORT(oobhandler, session, vartype="prop", *args, **kwargs):
+##OOB{"UNREPORT": "TEST"}
+def UNREPORT(oobhandler, session, *args, **kwargs):
"""
This removes tracking for the given data given in *args.
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
- typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)
- if typ == "field":
- oobhandler.untrack_field(obj, session.sessid, name)
+ trackname = OOB_REPORTABLE.get(name, None)
+ if not trackname:
+ session.msg(oob=("err", ("No Un-Reportable property '%s'. Use LIST REPORTED_VALUES." % name,)))
+ elif trackname.startswith("db_"):
+ oobhandler.untrack_field(obj, session.sessid, trackname)
else: # assume attribute
- oobhandler.untrack_attribute(obj, session.sessid, name)
+ oobhandler.untrack_attribute(obj, session.sessid, trackname)
+ else:
+ session.msg(oob=("err", ("You must log in first.",)))
+##OOB{"LIST":"COMMANDS"}
def LIST(oobhandler, session, mode, *args, **kwargs):
"""
List available properties. Mode is the type of information
@@ -135,15 +153,42 @@ def LIST(oobhandler, session, mode, *args, **kwargs):
session.msg(oob=("list", ("REPORTABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "REPORTED_VARIABLES":
- session.msg(oob=("list", ("REPORTED_VARIABLES",) +
- tuple(oobhandler.get_all_tracked(session))))
+ # we need to check so as to use the right return value depending on if it is
+ # an Attribute (identified by tracking the db_value field) or a normal database field
+ reported = oobhandler.get_all_tracked(session)
+ reported = [stored[2] if stored[2] != "db_value" else stored[4][0] for stored in reported]
+ session.msg(oob=("list", ["REPORTED_VARIABLES"] + reported))
elif mode == "SENDABLE_VARIABLES":
session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
- #elif mode == "CONFIGURABLE_VARIABLES":
- # pass
+ elif mode == "CONFIGURABLE_VARIABLES":
+ # Not implemented (game specific)
+ pass
else:
- session.msg(oob=("list", ("unsupported mode",)))
+ session.msg(oob=("err", ("LIST", "Unsupported mode",)))
+
+def _repeat_callback(oobhandler, session, *args, **kwargs):
+ "Set up by REPEAT"
+ session.msg(oob=("repeat", ("Repeat!",)))
+
+##OOB{"REPEAT":10}
+def REPEAT(oobhandler, session, interval, *args, **kwargs):
+ """
+ Test command for the repeat functionality. Note that the args/kwargs
+ must not be db objects (or anything else non-picklable), rather use
+ dbrefs if so needed. The callback must be defined globally and
+ will be called as
+ callback(oobhandler, session, *args, **kwargs)
+ """
+ oobhandler.repeat(None, session.sessid, interval, _repeat_callback, *args, **kwargs)
+
+
+##OOB{"UNREPEAT":10}
+def UNREPEAT(oobhandler, session, interval):
+ """
+ Disable repeating callback
+ """
+ oobhandler.unrepeat(None, session.sessid, interval)
# Mapping for how to retrieve each property name.
@@ -167,9 +212,10 @@ OOB_SENDABLE = {
"UTF_8": lambda o: True
}
-# mapping for which properties may be tracked. Each callable should return a tuple (type, value) where
-# the type is one of "field" or "attribute" depending on what is being tracked.
+# mapping for which properties may be tracked. Each value points either to a database field
+# (starting with db_*) or an Attribute name.
OOB_REPORTABLE = {
- "CHARACTER_NAME": lambda o: ("field", o.key),
- "ROOM_NAME": lambda o: ("attribute", o.db_location.key)
+ "CHARACTER_NAME": "db_key",
+ "ROOM_NAME": "db_location",
+ "TEST" : "test"
}
diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py
index aeacfe3c04..1dc4aca882 100644
--- a/src/server/oobhandler.py
+++ b/src/server/oobhandler.py
@@ -36,7 +36,6 @@ messages.
from inspect import isfunction
from twisted.internet.defer import inlineCallbacks
-from twisted.internet.task import LoopingCall
from django.conf import settings
from src.server.models import ServerConfig
from src.server.sessionhandler import SESSIONS
@@ -57,12 +56,12 @@ for mod in make_iter(settings.OOB_PLUGIN_MODULES):
_OOB_FUNCS.update(dict((key.lower(), func) for key, func in all_from_module(mod).items() if isfunction(func)))
# get custom error method or use the default
-_OOB_ERROR = _OOB_FUNCS.get("_OOB_ERROR", None)
+_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"Error wrapper"
- session.msg(oob=("send", {"ERROR": errmsg}))
+ session.msg(oob=("err", ("ERROR ", errmsg)))
_OOB_ERROR = oob_error
@@ -105,16 +104,16 @@ class TrackerHandler(object):
def remove(self, fieldname, trackerclass, *args, **kwargs):
"""
- Remove tracker from handler. Raises KeyError if tracker
- is not found.
+ Remove identified tracker from TrackerHandler.
+ Raises KeyError if tracker is not found.
"""
trackerkey = trackerclass.__name__
tracker = self.tracktargets[fieldname][trackerkey]
try:
- tracker.at_delete(*args, **kwargs)
+ tracker.at_remove(*args, **kwargs)
except Exception:
logger.log_trace()
- del tracker
+ del self.tracktargets[fieldname][trackerkey]
self.ntrackers -= 1
if self.ntrackers <= 0:
# if there are no more trackers, clean this handler
@@ -173,9 +172,9 @@ class ReportFieldTracker(TrackerBase):
new_value = new_value.key
except AttributeError:
new_value = to_str(new_value, force_string=True)
+ kwargs[self.fieldname] = new_value
# this is a wrapper call for sending oob data back to session
- self.oobhandler.msg(self.sessid, "report", self.fieldname,
- new_value, *args, **kwargs)
+ self.oobhandler.msg(self.sessid, "report", *args, **kwargs)
class ReportAttributeTracker(TrackerBase):
@@ -199,8 +198,9 @@ class ReportAttributeTracker(TrackerBase):
new_value = new_value.dbobj
except AttributeError:
new_value = to_str(new_value, force_string=True)
+ kwargs[self.attrname] = new_value
# this is a wrapper call for sending oob data back to session
- self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
+ self.oobhandler.msg(self.sessid, "report", *args, **kwargs)
@@ -208,25 +208,21 @@ class ReportAttributeTracker(TrackerBase):
class OOBTicker(Ticker):
"""
- Version of Ticker that calls OOB_FUNC rather than trying to call
+ Version of Ticker that executes an executable rather than trying to call
a hook method.
"""
@inlineCallbacks
- def _callback(self, oobhandler, sessions):
+ def _callback(self):
"See original for more info"
for key, (_, args, kwargs) in self.subscriptions.items():
- session = sessions.session_from_sessid(kwargs.get("sessid"))
+ # args = (sessid, callback_function)
+ session = SESSIONS.session_from_sessid(args[0])
try:
- oobhandler.execute_cmd(session, kwargs.get("func_key"), *args, **kwargs)
+ # execute the oob callback
+ yield args[1](OOB_HANDLER, session, *args[2:], **kwargs)
except Exception:
logger.log_trace()
- def __init__(self, interval):
- "Sets up the Ticker"
- self.interval = interval
- self.subscriptions = {}
- self.task = LoopingCall(self._callback, OOB_HANDLER, SESSIONS)
-
class OOBTickerPool(TickerPool):
ticker_class = OOBTicker
@@ -270,9 +266,9 @@ class OOBHandler(object):
tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage")
if tracker_storage:
self.oob_tracker_storage = dbunserialize(tracker_storage)
- #print "recovered from tracker_storage:", self.oob_tracker_storage
for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values():
- self.track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs)
+ #print "restoring tracking:",obj, sessid, fieldname, trackerclass
+ self._track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs)
# make sure to purge the storage
ServerConfig.objects.conf(key="oob_tracker_storage", delete=True)
self.tickerhandler.restore()
@@ -302,6 +298,7 @@ class OOBHandler(object):
storekey = (obj_packed, sessid, propname)
stored = (obj_packed, sessid, propname, trackerclass, args, kwargs)
self.oob_tracker_storage[storekey] = stored
+ #print "_track:", obj, id(obj), obj.__dict__
def _untrack(self, obj, sessid, propname, trackerclass, *args, **kwargs):
"""
@@ -312,9 +309,8 @@ class OOBHandler(object):
obj = obj.dbobj
except AttributeError:
pass
-
try:
- # call at_delete hook
+ # call at_remove hook on the trackerclass
_GA(obj, "_trackerhandler").remove(propname, trackerclass, *args, **kwargs)
except AttributeError:
pass
@@ -327,7 +323,7 @@ class OOBHandler(object):
Get the names of all variables this session is tracking.
"""
sessid = session.sessid
- return [key[2].lstrip("db_") for key in self.oob_tracker_storage.keys() if key[1] == sessid]
+ return [stored for key, stored in self.oob_tracker_storage.items() if key[1] == sessid]
def track_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
@@ -336,14 +332,14 @@ class OOBHandler(object):
"""
# all database field names starts with db_*
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
- self._track(obj, sessid, field_name, trackerclass)
+ self._track(obj, sessid, field_name, trackerclass, field_name)
- def untrack_field(self, obj, sessid, field_name):
+ def untrack_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
Shortcut for untracking a database field. Uses OOBTracker by defualt
"""
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
- self._untrack(obj, sessid, field_name)
+ self._untrack(obj, sessid, field_name, trackerclass)
def track_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
@@ -353,14 +349,15 @@ class OOBHandler(object):
"""
# get the attribute object if we can
try:
- obj = obj.dbobj
+ attrobj = obj.dbobj
except AttributeError:
pass
- attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
+ attrobj = obj.attributes.get(attr_name, return_obj=True)
+ #print "track_attribute attrobj:", attrobj, id(attrobj)
if attrobj:
self._track(attrobj, sessid, "db_value", trackerclass, attr_name)
- def untrack_attribute(self, obj, sessid, attr_name, trackerclass):
+ def untrack_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
Shortcut for deactivating tracking for a given attribute.
"""
@@ -368,25 +365,24 @@ class OOBHandler(object):
obj = obj.dbobj
except AttributeError:
pass
- attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
+ attrobj = obj.attributes.get(attr_name, return_obj=True)
if attrobj:
- self._untrack(attrobj, sessid, attr_name, trackerclass)
+ self._untrack(attrobj, sessid, "db_value", trackerclass, attr_name)
- def repeat(self, obj, sessid, func_key, interval=20, *args, **kwargs):
+ def repeat(self, obj, sessid, interval=20, callback=None, *args, **kwargs):
"""
- Start a repeating action. Every interval seconds,
- the oobfunc corresponding to func_key is called with
- args and kwargs.
+ Start a repeating action. Every interval seconds, trigger
+ callback(*args, **kwargs). The callback is called with
+ args and kwargs; note that *args and **kwargs may not contain
+ anything un-picklable (use dbrefs if wanting to use objects).
"""
- if not func_key in _OOB_FUNCS:
- raise KeyError("%s is not a valid OOB function name.")
- self.tickerhandler.add(self, obj, interval, func_key=func_key, sessid=sessid, *args, **kwargs)
+ self.tickerhandler.add(obj, interval, sessid, callback, *args, **kwargs)
- def unrepeat(self, obj, sessid, func_key, interval=20):
+ def unrepeat(self, obj, sessid, interval=20):
"""
Stop a repeating action
"""
- self.tickerhandler.remove(self, obj, interval)
+ self.tickerhandler.remove(obj, interval)
# access method - called from session.msg()
@@ -396,23 +392,26 @@ class OOBHandler(object):
Retrieve oobfunc from OOB_FUNCS and execute it immediately
using *args and **kwargs
"""
- try:
- #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
- oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
- oobfunc(self, session, *args, **kwargs)
- except KeyError,e:
- errmsg = "OOB Error: function '%s' not recognized: %s" % (func_key, e)
+ oobfunc = _OOB_FUNCS.get(func_key, None)
+ if not oobfunc:
+ # function not found
+ errmsg = "OOB Error: function '%s' not recognized." % func_key
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
+ logger.log_trace()
else:
logger.log_trace(errmsg)
- raise KeyError(errmsg)
+ return
+
+ # execute the found function
+ try:
+ #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
+ oobfunc(self, session, *args, **kwargs)
except Exception, err:
errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err)
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
- else:
- logger.log_trace(errmsg)
+ logger.log_trace(errmsg)
raise Exception(errmsg)
def msg(self, sessid, funcname, *args, **kwargs):
diff --git a/src/server/portal/websocket_client.py b/src/server/portal/websocket_client.py
index 2fd8c158f9..d3a7bd553a 100644
--- a/src/server/portal/websocket_client.py
+++ b/src/server/portal/websocket_client.py
@@ -31,7 +31,7 @@ import json
from twisted.internet.protocol import Protocol
from src.server.session import Session
from src.utils.logger import log_trace
-from src.utils.utils import to_str
+from src.utils.utils import to_str, make_iter
from src.utils.text2html import parse_html
@@ -85,7 +85,8 @@ class WebSocketClient(Protocol, Session):
try:
oobdata = json.loads(string)
for (key, args) in oobdata.items():
- self.data_in(text=None, oob=(key, args))
+ #print "oob data in:", (key, args)
+ self.data_in(text=None, oob=(key, make_iter(args)))
except Exception:
log_trace("Websocket malformed OOB request: %s" % string)
else:
@@ -119,6 +120,7 @@ class WebSocketClient(Protocol, Session):
self.sendLine(str(e))
if "oob" in kwargs:
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
+ #print "oob data_out:", "OOB" + json.dumps(oobstruct)
self.sendLine("OOB" + json.dumps(oobstruct))
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
diff --git a/src/server/server.py b/src/server/server.py
index 9a0c7f1ee5..bc0f8c3e3a 100644
--- a/src/server/server.py
+++ b/src/server/server.py
@@ -33,9 +33,9 @@ from src.server.sessionhandler import SESSIONS
# setting up server-side field cache
from django.db.models.signals import post_save
-from src.server.caches import field_pre_save
+from src.server.caches import field_post_save
#pre_save.connect(field_pre_save, dispatch_uid="fieldcache")
-post_save.connect(field_pre_save, dispatch_uid="fieldcache")
+post_save.connect(field_post_save, dispatch_uid="fieldcache")
#from src.server.caches import post_attr_update
#from django.db.models.signals import m2m_changed
diff --git a/src/server/serversession.py b/src/server/serversession.py
index ce04bfd0fa..00551f4b42 100644
--- a/src/server/serversession.py
+++ b/src/server/serversession.py
@@ -206,6 +206,7 @@ class ServerSession(Session):
if not _OOB_HANDLER:
from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None))
+ #print "session.data_in: oobstruct:",oobstruct
for (funcname, args, kwargs) in oobstruct:
if funcname:
_OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs)
diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py
index b911226095..2dc3edd152 100644
--- a/src/server/sessionhandler.py
+++ b/src/server/sessionhandler.py
@@ -136,14 +136,17 @@ class SessionHandler(object):
elif isinstance(oobstruct[1], (tuple, list)):
# cmdname, (args,)
return (oobstruct[0].lower(), list(oobstruct[1]), {})
+ else:
+ # cmdname, cmdname
+ return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {}))
else:
# cmdname, (args,), {kwargs}
return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2]))
if hasattr(oobstruct, "__iter__"):
# differentiate between (cmdname, cmdname),
- # (cmdname, args, kwargs) and ((cmdname,args,kwargs),
- # (cmdname,args,kwargs), ...)
+ # (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}),
+ # (cmdname,(args),{kwargs}), ...)
if oobstruct and isinstance(oobstruct[0], basestring):
return (list(_parse(oobstruct)),)
diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py
index c4b03a743d..5abea472e9 100644
--- a/src/typeclasses/models.py
+++ b/src/typeclasses/models.py
@@ -70,7 +70,7 @@ _DA = object.__delattr__
#------------------------------------------------------------
#class Attribute(SharedMemoryModel):
-class Attribute(WeakSharedMemoryModel):
+class Attribute(SharedMemoryModel):
"""
Abstract django model.
@@ -173,13 +173,6 @@ class Attribute(WeakSharedMemoryModel):
"""
self.db_value = to_pickle(new_value)
self.save(update_fields=["db_value"])
- try:
- # eventual OOB hook
- #self._track_db_value_change.update(self.cached_value)
- self._track_db_value_change.update(self.new_value)
- except AttributeError:
- pass
- return
#@value.deleter
def __value_del(self):
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index b604fc92fc..e913f65e5b 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -20,13 +20,38 @@ var OOB_debug = true
// Custom OOB functions
// functions defined here can be called by name by the server. For
-// example the OOB{"echo":arguments} will trigger a function named
-// echo(arguments).
+// example the OOB{"echo":(args),{kwargs}} will trigger a function named
+// echo(args, kwargs).
-function echo(message) {
+function echo(args, kwargs) {
// example echo function.
- doShow("out", "ECHO return: " + message) }
+ doShow("out", "ECHO return: " + args) }
+
+function list (args, kwargs) {
+ // show in main window
+ doShow("out", args) }
+
+function send (args, kwargs) {
+ // show in main window. SEND returns kwargs {name:value}.
+ for (sendvalue in kwargs) {
+ doShow("out", sendvalue + " = " + kwargs[sendvalue]);}
+}
+
+function report (args, kwargs) {
+ // show in main window. REPORT returns kwargs
+ // {attrfieldname:value}
+ for (name in kwargs) {
+ doShow("out", name + " = " + kwargs[name]) }
+}
+
+function repeat (args, kwargs) {
+ // called by repeating oob funcs
+ doShow("out", args) }
+
+function err (args, kwargs) {
+ // display error
+ doShow("err", args) }
// Webclient code
@@ -61,15 +86,20 @@ function onMessage(evt) {
var inmsg = evt.data
if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
// dynamically call oob methods, if available
- try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB }
+ try {
+ var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB }
catch(err) {
// not JSON packed - a normal text
- doShow('out', err + " " + inmsg);
+ doShow('out', inmsg);
return;
}
- for (var ind in oobarray) {
- try { window[oobarray[ind][0]](oobarray[ind][1]) }
- catch(err) { doShow("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") }
+ if (typeof oobarray != "undefined") {
+ for (var ind in oobarray) {
+ try {
+ window[oobarray[ind][0]](oobarray[ind][1], oobarray[ind][2]) }
+ catch(err) {
+ doShow("err", "Could not execute js OOB function '" + oobarray[ind][0] + "(" + oobarray[ind][1] + oobarray[ind][2] + ")'") }
+ }
}
}
else {
@@ -93,8 +123,15 @@ function doSend(){
if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
// test OOB messaging
- doShow("out", "OOB input: " + outmsg.slice(5))
- doOOB(JSON.parse(outmsg.slice(5))); }
+ try {
+ doShow("out", "OOB input: " + outmsg.slice(5));
+ if (outmsg.length == 5) {
+ doShow("err", "OOB testing syntax: ##OOB{\"cmdname:args, ...}"); }
+ else {
+ doOOB(JSON.parse(outmsg.slice(5))); } }
+ catch(err) {
+ doShow("err", err) }
+ }
else {
// normal output
websocket.send(outmsg); }
From 8c5621025b72bde1c37a74a34563f363ecc0b74a Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sat, 28 Jun 2014 18:08:22 +0200
Subject: [PATCH 10/25] Added oob test command for websocket client.
---
.../javascript/evennia_websocket_webclient.js | 21 +++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js
index e913f65e5b..b63b44b9ae 100644
--- a/src/web/media/javascript/evennia_websocket_webclient.js
+++ b/src/web/media/javascript/evennia_websocket_webclient.js
@@ -18,10 +18,12 @@ messages sent to the client is one of two modes:
// prepending with ##OOB{}, for example ##OOB{"echo":[1,2,3,4]}
var OOB_debug = true
+//
// Custom OOB functions
// functions defined here can be called by name by the server. For
-// example the OOB{"echo":(args),{kwargs}} will trigger a function named
-// echo(args, kwargs).
+// example input OOB{"echo":(args),{kwargs}} will trigger a function named
+// echo(args, kwargs). The commands the server understands is set by
+// settings.OOB_PLUGIN_MODULES
function echo(args, kwargs) {
@@ -54,7 +56,9 @@ function err (args, kwargs) {
doShow("err", args) }
+//
// Webclient code
+//
function webclient_init(){
// called when client is just initializing
@@ -122,6 +126,19 @@ function doSend(){
$('#inputform')[0].reset(); // clear input field
if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
+ if (outmsg == "##OOBUNITTEST") {
+ // unittest mode
+ doShow("out", "OOB testing mode ...");
+ doOOB(JSON.parse('{"ECHO":"Echo test"}'));
+ doOOB(JSON.parse('{"LIST":"COMMANDS"}'));
+ doOOB(JSON.parse('{"SEND":"CHARACTER_NAME"}'));
+ doOOB(JSON.parse('{"REPORT":"TEST"}'));
+ doOOB(JSON.parse('{"UNREPORT":"TEST"}'));
+ doOOB(JSON.parse('{"REPEAT": 1}'));
+ doOOB(JSON.parse('{"UNREPEAT": 1}'));
+ doShow("out", "... OOB testing mode done.");
+ return
+ }
// test OOB messaging
try {
doShow("out", "OOB input: " + outmsg.slice(5));
From b092d29989e676a20ca8053e1e49240d3086b86b Mon Sep 17 00:00:00 2001
From: n0q
Date: Thu, 26 Jun 2014 20:08:23 -0400
Subject: [PATCH 11/25] Fixed page command to properly repage
Page command was attempting to first page players, then repage players,
then repage chracaters, then fail. Should reliably repage players, now.
Does not permit paging characters.
---
src/commands/default/comms.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py
index 661cd91cdd..f77b7ad272 100644
--- a/src/commands/default/comms.py
+++ b/src/commands/default/comms.py
@@ -712,7 +712,7 @@ class CmdPage(MuxPlayerCommand):
if isinstance(receiver, basestring):
pobj = caller.search(receiver)
elif hasattr(receiver, 'character'):
- pobj = receiver.character
+ pobj = receiver
else:
self.msg("Who do you want to page?")
return
@@ -741,13 +741,13 @@ class CmdPage(MuxPlayerCommand):
rstrings.append("You are not allowed to page %s." % pobj)
continue
pobj.msg("%s %s" % (header, message))
- if hasattr(pobj, 'has_player') and not pobj.has_player:
+ if hasattr(pobj, 'sessions') and not pobj.sessions:
received.append("{C%s{n" % pobj.name)
rstrings.append("%s is offline. They will see your message if they list their pages later." % received[-1])
else:
received.append("{c%s{n" % pobj.name)
if rstrings:
- self.msg(rstrings="\n".join(rstrings))
+ self.msg("\n".join(rstrings))
self.msg("You paged %s with: '%s'." % (", ".join(received), message))
From bb8c9ce2ab64caac000ec2a2ea89770b2d3f9d99 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sat, 28 Jun 2014 18:48:54 +0200
Subject: [PATCH 12/25] Changed default puppet lock to puppet:pperm(Immortals)
by default. Resolves #521.
---
src/objects/objects.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/objects/objects.py b/src/objects/objects.py
index 1a7d09facf..dd0a978243 100644
--- a/src/objects/objects.py
+++ b/src/objects/objects.py
@@ -526,7 +526,6 @@ class Object(TypeClass):
# commands may set this (create an item and you should be its
# controller, for example)
- dbref = self.dbobj.dbref
self.locks.add(";".join([
"control:perm(Immortals)", # edit locks/permissions, delete
"examine:perm(Builders)", # examine properties
@@ -536,8 +535,7 @@ class Object(TypeClass):
"get:all()", # pick up object
"call:true()", # allow to call commands on this object
"tell:perm(Wizards)", # allow emits to this object
- # restricts puppeting of this object
- "puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref]))
+ "puppet:pperm(Immortals)"])) # lock down puppeting only to staff by default
def basetype_posthook_setup(self):
"""
From 05c9d67ffc65d06379031345f0c4c299f7daf631 Mon Sep 17 00:00:00 2001
From: Griatch
Date: Sat, 28 Jun 2014 18:51:04 +0200
Subject: [PATCH 13/25] Made the tab, space and linebreak ansi characters
case-insensitive. Resolves #522.
---
src/utils/ansi.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/utils/ansi.py b/src/utils/ansi.py
index 090cd37596..72b6f8006a 100644
--- a/src/utils/ansi.py
+++ b/src/utils/ansi.py
@@ -223,8 +223,11 @@ class ANSIParser(object):
(r'%cn', ANSI_NORMAL),
(r'%ch', ANSI_HILITE),
(r'%r', ANSI_RETURN),
+ (r'%R', ANSI_RETURN),
(r'%t', ANSI_TAB),
+ (r'%T', ANSI_TAB),
(r'%b', ANSI_SPACE),
+ (r'%B', ANSI_SPACE),
(r'%cf', ANSI_BLINK), # annoying and not supported by all clients
(r'%ci', ANSI_INVERSE),
From 16bcc3c9f04193c8fae11b6ec835a7a74510b277 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Fri, 2 May 2014 22:52:12 -0500
Subject: [PATCH 14/25] Restructured web resources to better manage static
files.
---
.../website => game/gamesrc/web}/__init__.py | 0
game/gamesrc/web/static/README.md | 5 ++++
game/gamesrc/web/urls.py | 13 +++++++++
src/server/portal/webclient.py | 2 +-
src/server/server.py | 2 ++
src/settings_default.py | 13 ++++++---
src/web/news/urls.py | 11 ++++----
.../ev}/css/prosimii-print.css | 0
.../ev}/css/prosimii-screen-alt.css | 0
.../ev}/css/prosimii-screen.css | 0
src/web/{media => static/ev}/images/LICENCE | 0
.../ev}/images/evennia_logo.png | Bin
.../ev}/images/evennia_logo_small.png | Bin
.../{media => static/ev}/images/favicon.ico | Bin
src/web/templates/admin/base_site.html | 2 +-
src/web/templates/prosimii/base.html | 23 ++++++++--------
src/web/templates/prosimii/index.html | 4 +--
src/web/urls.py | 25 +++++++++---------
src/web/{website => }/views.py | 2 +-
.../static/webclient}/css/webclient.css | 0
.../static/webclient/js/evennia_webclient.js} | 0
.../templates}/webclient.html | 6 ++---
src/web/webclient/urls.py | 4 +--
src/web/website/models.py | 7 -----
src/web/website/urls.py | 10 -------
25 files changed, 70 insertions(+), 59 deletions(-)
rename {src/web/website => game/gamesrc/web}/__init__.py (100%)
create mode 100644 game/gamesrc/web/static/README.md
create mode 100644 game/gamesrc/web/urls.py
rename src/web/{media => static/ev}/css/prosimii-print.css (100%)
rename src/web/{media => static/ev}/css/prosimii-screen-alt.css (100%)
rename src/web/{media => static/ev}/css/prosimii-screen.css (100%)
rename src/web/{media => static/ev}/images/LICENCE (100%)
rename src/web/{media => static/ev}/images/evennia_logo.png (100%)
rename src/web/{media => static/ev}/images/evennia_logo_small.png (100%)
rename src/web/{media => static/ev}/images/favicon.ico (100%)
rename src/web/{website => }/views.py (99%)
rename src/web/{media => webclient/static/webclient}/css/webclient.css (100%)
rename src/web/{media/javascript/evennia_ajax_webclient.js => webclient/static/webclient/js/evennia_webclient.js} (100%)
rename src/web/{templates/prosimii => webclient/templates}/webclient.html (92%)
delete mode 100644 src/web/website/models.py
delete mode 100644 src/web/website/urls.py
diff --git a/src/web/website/__init__.py b/game/gamesrc/web/__init__.py
similarity index 100%
rename from src/web/website/__init__.py
rename to game/gamesrc/web/__init__.py
diff --git a/game/gamesrc/web/static/README.md b/game/gamesrc/web/static/README.md
new file mode 100644
index 0000000000..8ba3d8ff20
--- /dev/null
+++ b/game/gamesrc/web/static/README.md
@@ -0,0 +1,5 @@
+This folder is used by Django's staticfiles application for gathering all static files (CSS, JS, etc) into one place
+from the various sources that they might be gathered from.
+
+Do not edit files in this directory. Instead, read up on Django's static file handling to learn how to create Django
+apps with their own static files.
\ No newline at end of file
diff --git a/game/gamesrc/web/urls.py b/game/gamesrc/web/urls.py
new file mode 100644
index 0000000000..8e76e279c9
--- /dev/null
+++ b/game/gamesrc/web/urls.py
@@ -0,0 +1,13 @@
+from src.web.urls import urlpatterns
+#
+# File that determines what each URL points to. This uses _Python_ regular
+# expressions, not Perl's.
+#
+# See:
+# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
+#
+
+# You can add URLs to your game's website by extending urlpatterns.
+#
+# These are Django URL patterns, so you should look up how to use these at
+# https://docs.djangoproject.com/en/1.6/topics/http/urls/
\ No newline at end of file
diff --git a/src/server/portal/webclient.py b/src/server/portal/webclient.py
index c5f34558ed..74c32c6a4f 100644
--- a/src/server/portal/webclient.py
+++ b/src/server/portal/webclient.py
@@ -18,6 +18,7 @@ found on http://localhost:8000/webclient.)
"""
import time
import json
+
from hashlib import md5
from twisted.web import server, resource
@@ -32,7 +33,6 @@ from src.server import session
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
-
# defining a simple json encoder for returning
# django data to the client. Might need to
# extend this if one wants to send more
diff --git a/src/server/server.py b/src/server/server.py
index bc0f8c3e3a..5e7166ce1e 100644
--- a/src/server/server.py
+++ b/src/server/server.py
@@ -411,6 +411,8 @@ if WEBSERVER_ENABLED:
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
+ # point our static resources to url /static
+ web_root.putChild("static", static.File(settings.STATIC_ROOT))
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for proxyport, serverport in WEBSERVER_PORTS:
diff --git a/src/settings_default.py b/src/settings_default.py
index 9a6d466a52..1f7b79af19 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -480,7 +480,7 @@ LOCALE_PATHS = ["../locale/"]
SERVE_MEDIA = False
# The master urlconf file that contains all of the sub-branches to the
# applications.
-ROOT_URLCONF = 'src.web.urls'
+ROOT_URLCONF = 'game.gamesrc.web.urls'
# Where users are redirected after logging in via contrib.auth.login.
LOGIN_REDIRECT_URL = '/'
# Where to redirect users when using the @login_required decorator.
@@ -493,7 +493,13 @@ MEDIA_URL = '/media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure
# to use a trailing slash. Django1.4+ will look for admin files under
# STATIC_URL/admin.
-STATIC_URL = '/media/'
+STATIC_URL = '/static/'
+
+STATIC_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "static")
+
+# Folders from which static files will be gathered from.
+STATICFILES_DIRS = (
+ os.path.join(SRC_DIR, "web", "static"),)
# The name of the currently selected web template. This corresponds to the
# directory names shown in the webtemplates directory.
ACTIVE_TEMPLATE = 'prosimii'
@@ -540,6 +546,7 @@ INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.admindocs',
'django.contrib.flatpages',
+ 'django.contrib.staticfiles',
'src.server',
'src.typeclasses',
'src.players',
@@ -548,7 +555,7 @@ INSTALLED_APPS = (
'src.help',
'src.scripts',
'src.web.news',
- 'src.web.website',)
+ 'src.web.webclient')
# The user profile extends the User object with more functionality;
# This should usually not be changed.
AUTH_USER_MODEL = "players.PlayerDB"
diff --git a/src/web/news/urls.py b/src/web/news/urls.py
index 09c4a3b6fe..d0a769531f 100755
--- a/src/web/news/urls.py
+++ b/src/web/news/urls.py
@@ -5,9 +5,8 @@ It is imported from the root handler, game.web.urls.py.
from django.conf.urls import *
-urlpatterns = patterns('src.web.news.views',
- (r'^show/(?P\d+)/$', 'show_news'),
- (r'^archive/$', 'news_archive'),
- (r'^search/$', 'search_form'),
- (r'^search/results/$', 'search_results'),
-)
+urlpatterns = [
+ url(r'^show/(?P\d+)/$', 'show_news', name="show"),
+ url(r'^archive/$', 'news_archive', name="archive"),
+ url(r'^search/$', 'search_form', name="search"),
+ url(r'^search/results/$', 'search_results', name="search_results")]
diff --git a/src/web/media/css/prosimii-print.css b/src/web/static/ev/css/prosimii-print.css
similarity index 100%
rename from src/web/media/css/prosimii-print.css
rename to src/web/static/ev/css/prosimii-print.css
diff --git a/src/web/media/css/prosimii-screen-alt.css b/src/web/static/ev/css/prosimii-screen-alt.css
similarity index 100%
rename from src/web/media/css/prosimii-screen-alt.css
rename to src/web/static/ev/css/prosimii-screen-alt.css
diff --git a/src/web/media/css/prosimii-screen.css b/src/web/static/ev/css/prosimii-screen.css
similarity index 100%
rename from src/web/media/css/prosimii-screen.css
rename to src/web/static/ev/css/prosimii-screen.css
diff --git a/src/web/media/images/LICENCE b/src/web/static/ev/images/LICENCE
similarity index 100%
rename from src/web/media/images/LICENCE
rename to src/web/static/ev/images/LICENCE
diff --git a/src/web/media/images/evennia_logo.png b/src/web/static/ev/images/evennia_logo.png
similarity index 100%
rename from src/web/media/images/evennia_logo.png
rename to src/web/static/ev/images/evennia_logo.png
diff --git a/src/web/media/images/evennia_logo_small.png b/src/web/static/ev/images/evennia_logo_small.png
similarity index 100%
rename from src/web/media/images/evennia_logo_small.png
rename to src/web/static/ev/images/evennia_logo_small.png
diff --git a/src/web/media/images/favicon.ico b/src/web/static/ev/images/favicon.ico
similarity index 100%
rename from src/web/media/images/favicon.ico
rename to src/web/static/ev/images/favicon.ico
diff --git a/src/web/templates/admin/base_site.html b/src/web/templates/admin/base_site.html
index 578579e79b..0fd56d237a 100644
--- a/src/web/templates/admin/base_site.html
+++ b/src/web/templates/admin/base_site.html
@@ -5,7 +5,7 @@
{% block branding %}
{% trans 'Evennia database administration' %}
- (Back)
@@ -87,7 +88,7 @@
title="Other designs by haran">haran.
Powered by
Evennia.
-
+
diff --git a/src/web/templates/prosimii/index.html b/src/web/templates/prosimii/index.html
index 2594b742f3..19e69d8238 100644
--- a/src/web/templates/prosimii/index.html
+++ b/src/web/templates/prosimii/index.html
@@ -14,11 +14,11 @@
Welcome to your new installation of Evennia, your friendly
neighborhood next-generation MUD development system and server. You are looking at Evennia's web
presence, which can be expanded to a full-fledged site as
- needed. Through the admin interface you can view and edit the
+ needed. Through the admin interface you can view and edit the
database without logging into the game.
{% if webclient_enabled %}
You can also connect to the game directly from your browser using our
- online client!
+ online client!
{% endif %}
For more info, take your time to
peruse our extensive online documentation.
diff --git a/src/web/urls.py b/src/web/urls.py
index 9d9d1844c3..dd1a654e27 100755
--- a/src/web/urls.py
+++ b/src/web/urls.py
@@ -24,18 +24,16 @@ admin.autodiscover()
# Setup the root url tree from /
-urlpatterns = patterns('',
+urlpatterns = [
# User Authentication
- url(r'^accounts/login', 'django.contrib.auth.views.login'),
- url(r'^accounts/logout', 'django.contrib.auth.views.logout'),
+ url(r'^accounts/login', 'django.contrib.auth.views.login', name="login"),
+ url(r'^accounts/logout', 'django.contrib.auth.views.logout', name="logout"),
- # Front page
- url(r'^', include('src.web.website.urls')),
# News stuff
# url(r'^news/', include('src.web.news.urls')),
# Page place-holder for things that aren't implemented yet.
- url(r'^tbi/', 'src.web.website.views.to_be_implemented'),
+ url(r'^tbi/', 'src.web.views.to_be_implemented', name='to_be_implemented'),
# Admin interface
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
@@ -44,13 +42,16 @@ urlpatterns = patterns('',
# favicon
url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')),
- # webclient stuff
- url(r'^webclient/', include('src.web.webclient.urls')),
-)
+ # ajax stuff
+ url(r'^webclient/', include('src.web.webclient.urls', namespace='webclient', app_name='webclient')),
+
+ # Front page
+ url(r'^$', 'src.web.views.page_index', name="index")]
# This sets up the server if the user want to run the Django
# test server (this should normally not be needed).
if settings.SERVE_MEDIA:
- urlpatterns += patterns('',
- (r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
- )
+ urlpatterns.extend([
+ url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
+ url(r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT})
+ ])
diff --git a/src/web/website/views.py b/src/web/views.py
similarity index 99%
rename from src/web/website/views.py
rename to src/web/views.py
index 2ab73c3d19..2126a74c55 100644
--- a/src/web/website/views.py
+++ b/src/web/views.py
@@ -33,7 +33,7 @@ def page_index(request):
nplyrs_conn_recent = len(recent_users) or "none"
nplyrs = PlayerDB.objects.num_total_players() or "none"
nplyrs_reg_recent = len(PlayerDB.objects.get_recently_created_players()) or "none"
- nsess = len(PlayerDB.objects.get_connected_players()) or "none"
+ nsess = len(PlayerDB.objects.get_connected_players()) or "no one"
nobjs = ObjectDB.objects.all().count()
nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()
diff --git a/src/web/media/css/webclient.css b/src/web/webclient/static/webclient/css/webclient.css
similarity index 100%
rename from src/web/media/css/webclient.css
rename to src/web/webclient/static/webclient/css/webclient.css
diff --git a/src/web/media/javascript/evennia_ajax_webclient.js b/src/web/webclient/static/webclient/js/evennia_webclient.js
similarity index 100%
rename from src/web/media/javascript/evennia_ajax_webclient.js
rename to src/web/webclient/static/webclient/js/evennia_webclient.js
diff --git a/src/web/templates/prosimii/webclient.html b/src/web/webclient/templates/webclient.html
similarity index 92%
rename from src/web/templates/prosimii/webclient.html
rename to src/web/webclient/templates/webclient.html
index f36839e708..efc1c90dfc 100644
--- a/src/web/templates/prosimii/webclient.html
+++ b/src/web/webclient/templates/webclient.html
@@ -1,4 +1,5 @@
+{% load staticfiles %}
@@ -6,7 +7,7 @@
Evennia web MUD client
-
+
@@ -23,14 +24,13 @@
document.write("\")}
else {
- document.write("\")}
+ document.write("\")}
{% else %}
{% endif %}
-
diff --git a/src/web/webclient/urls.py b/src/web/webclient/urls.py
index 03f2595d2e..ba5cdf8186 100644
--- a/src/web/webclient/urls.py
+++ b/src/web/webclient/urls.py
@@ -4,5 +4,5 @@ webpage 'application'.
"""
from django.conf.urls import *
-urlpatterns = patterns('',
- url(r'^$', 'src.web.webclient.views.webclient'),)
+urlpatterns = [
+ url(r'^$', 'src.web.webclient.views.webclient', name="index")]
diff --git a/src/web/website/models.py b/src/web/website/models.py
deleted file mode 100644
index 89e59e2200..0000000000
--- a/src/web/website/models.py
+++ /dev/null
@@ -1,7 +0,0 @@
-#
-# Define database entities for the app.
-#
-
-from django.db import models
-
-
diff --git a/src/web/website/urls.py b/src/web/website/urls.py
deleted file mode 100644
index 8bb4bd811e..0000000000
--- a/src/web/website/urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-This structures the (simple) structure of the
-webpage 'application'.
-"""
-
-from django.conf.urls import *
-
-urlpatterns = patterns('src.web.website.views',
- (r'^$', 'page_index'),
-)
From fbe0eab01ca99e99172539ff593839069ea06f11 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 25 May 2014 13:18:00 -0500
Subject: [PATCH 15/25] Some consistency improvements with URL overrides.
---
.gitignore | 7 ++++++
game/gamesrc/web/examples/__init__.py | 1 +
game/gamesrc/web/examples/urls.py | 28 +++++++++++++++++++++
game/gamesrc/web/static_overrides/README.md | 5 ++++
game/gamesrc/web/urls.py | 13 ----------
src/settings_default.py | 11 +++++---
src/web/urls.py | 2 +-
7 files changed, 50 insertions(+), 17 deletions(-)
create mode 100644 game/gamesrc/web/examples/__init__.py
create mode 100644 game/gamesrc/web/examples/urls.py
create mode 100644 game/gamesrc/web/static_overrides/README.md
delete mode 100644 game/gamesrc/web/urls.py
diff --git a/.gitignore b/.gitignore
index 878233d84f..c472b02630 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,10 @@ __pycache__
*.restart
*.db3
+# Installation-specific
+game/settings.py
+game/logs/*.log.*
+
# Installer logs
pip-log.txt
@@ -41,3 +45,6 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
+
+# PyCharm config
+.idea
diff --git a/game/gamesrc/web/examples/__init__.py b/game/gamesrc/web/examples/__init__.py
new file mode 100644
index 0000000000..cdcf9604a4
--- /dev/null
+++ b/game/gamesrc/web/examples/__init__.py
@@ -0,0 +1 @@
+__author__ = 'kelketek'
diff --git a/game/gamesrc/web/examples/urls.py b/game/gamesrc/web/examples/urls.py
new file mode 100644
index 0000000000..58754f51c9
--- /dev/null
+++ b/game/gamesrc/web/examples/urls.py
@@ -0,0 +1,28 @@
+from django.conf.urls import url, include
+
+from src.web.urls import urlpatterns
+
+#
+# File that determines what each URL points to. This uses _Python_ regular
+# expressions, not Perl's.
+#
+# See:
+# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
+#
+
+# Copy this file into your web directory in gamesrc, and add this line to settings.py:
+# ROOT_URLCONF = 'game.gamesrc.web.urls'
+
+# Add your own URL patterns to the patterns variable below, and then change
+#
+# These are Django URL patterns, so you should look up how to use these at
+# https://docs.djangoproject.com/en/1.6/topics/http/urls/
+
+# Follow the full Django tutorial to learn how to create web views for Evennia.
+# https://docs.djangoproject.com/en/1.6/intro/tutorial01/
+
+patterns = [
+ # url(r'/desired/url/', view, name='example'),
+]
+
+urlpatterns = patterns + urlpatterns
\ No newline at end of file
diff --git a/game/gamesrc/web/static_overrides/README.md b/game/gamesrc/web/static_overrides/README.md
new file mode 100644
index 0000000000..8640413887
--- /dev/null
+++ b/game/gamesrc/web/static_overrides/README.md
@@ -0,0 +1,5 @@
+If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, copy it into this folder, and it will be placed in the static folder when you run:
+
+ python manage.py collectstatic
+
+Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place.
diff --git a/game/gamesrc/web/urls.py b/game/gamesrc/web/urls.py
deleted file mode 100644
index 8e76e279c9..0000000000
--- a/game/gamesrc/web/urls.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from src.web.urls import urlpatterns
-#
-# File that determines what each URL points to. This uses _Python_ regular
-# expressions, not Perl's.
-#
-# See:
-# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
-#
-
-# You can add URLs to your game's website by extending urlpatterns.
-#
-# These are Django URL patterns, so you should look up how to use these at
-# https://docs.djangoproject.com/en/1.6/topics/http/urls/
\ No newline at end of file
diff --git a/src/settings_default.py b/src/settings_default.py
index 1f7b79af19..bc1e31d2c0 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -479,8 +479,8 @@ LOCALE_PATHS = ["../locale/"]
# development webserver (normally Evennia runs its own server)
SERVE_MEDIA = False
# The master urlconf file that contains all of the sub-branches to the
-# applications.
-ROOT_URLCONF = 'game.gamesrc.web.urls'
+# applications. Change this to add your own URLs to the website.
+ROOT_URLCONF = 'src.web.urls'
# Where users are redirected after logging in via contrib.auth.login.
LOGIN_REDIRECT_URL = '/'
# Where to redirect users when using the @login_required decorator.
@@ -497,14 +497,19 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "static")
-# Folders from which static files will be gathered from.
+# Directories from which static files will be gathered from.
STATICFILES_DIRS = (
+ os.path.join(GAME_DIR, "gamesrc", "web", "static_overrides"),
os.path.join(SRC_DIR, "web", "static"),)
+# Patterns of files in the static directories. Used here to make sure that
+# its readme file is preserved but unused.
+STATICFILES_IGNORE_PATTERNS = ('README.md',)
# The name of the currently selected web template. This corresponds to the
# directory names shown in the webtemplates directory.
ACTIVE_TEMPLATE = 'prosimii'
# We setup the location of the website template as well as the admin site.
TEMPLATE_DIRS = (
+ os.path.join(GAME_DIR, "gamesrc", "web", "templates"),
os.path.join(SRC_DIR, "web", "templates", ACTIVE_TEMPLATE),
os.path.join(SRC_DIR, "web", "templates"),)
# List of callables that know how to import templates from various sources.
diff --git a/src/web/urls.py b/src/web/urls.py
index dd1a654e27..788be59bb3 100755
--- a/src/web/urls.py
+++ b/src/web/urls.py
@@ -6,8 +6,8 @@
# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
#
-from django.conf.urls import *
from django.conf import settings
+from django.conf.urls import url, include
from django.contrib import admin
from django.views.generic import RedirectView
From 7d12c6dd69367a5fbee68b2d02415f845b743c19 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Wed, 4 Jun 2014 08:58:54 -0500
Subject: [PATCH 16/25] Added custom Evennia admin and ability to toggle it.
---
.gitignore | 1 +
src/settings_default.py | 9 +
src/web/templates/admin/base_site.html | 11 --
src/web/templates/admin/index.html | 184 ------------------
src/web/templates/admin/players/add_form.html | 14 --
.../templates/admin/players/change_form.html | 70 -------
.../templates/admin/players/change_list.html | 103 ----------
src/web/templates/admin/players/stacked.html | 82 --------
src/web/templates/prosimii/evennia_admin.html | 22 +++
src/web/urls.py | 18 +-
src/web/views.py | 30 ++-
11 files changed, 70 insertions(+), 474 deletions(-)
delete mode 100644 src/web/templates/admin/base_site.html
delete mode 100644 src/web/templates/admin/index.html
delete mode 100644 src/web/templates/admin/players/add_form.html
delete mode 100644 src/web/templates/admin/players/change_form.html
delete mode 100644 src/web/templates/admin/players/change_list.html
delete mode 100644 src/web/templates/admin/players/stacked.html
create mode 100644 src/web/templates/prosimii/evennia_admin.html
diff --git a/.gitignore b/.gitignore
index c472b02630..413011b83d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ __pycache__
# Installation-specific
game/settings.py
game/logs/*.log.*
+game/gamesrc/web/static/*
# Installer logs
pip-log.txt
diff --git a/src/settings_default.py b/src/settings_default.py
index bc1e31d2c0..d819a3b8cd 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -85,6 +85,15 @@ SSL_ENABLED = False
SSL_PORTS = [4001]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
SSL_INTERFACES = ['0.0.0.0']
+# Activate Websocket support
+WEBSOCKET_ENABLED = False
+# Ports to use for Websockets
+WEBSOCKET_PORTS = [8021]
+# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
+WEBSOCKET_INTERFACES = ['0.0.0.0']
+# This determine's whether Evennia's custom admin page is used, or if the
+# standard Django admin is used.
+EVENNIA_ADMIN = True
# The path that contains this settings.py file (no trailing slash).
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Path to the src directory containing the bulk of the codebase's code.
diff --git a/src/web/templates/admin/base_site.html b/src/web/templates/admin/base_site.html
deleted file mode 100644
index 0fd56d237a..0000000000
--- a/src/web/templates/admin/base_site.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "admin/base.html" %}
-{% load i18n %}
-
-{% block title %}{{ title }} | {% trans 'Evennia site admin' %}{% endblock %}
-
-{% block branding %}
-
{% trans 'Evennia database administration' %}
- (Back)
-
-{% if app_list %}
-
- {% for app in app_list %}
-
- {% if app.name in evennia_userapps %}
-
- {% if app.name == 'Players' %}
-
Admin
-
Players are the out-of-character representation of a
- game account. A Player can potentially control any number of
- in-game character Objects (depending on game).
If you are an advanced user who needs access to the raw Django Admin, it is available here.
+ You can make this the default my changing EVENNIA_ADMIN to False in settings.py and reload.
+{% endblock content %}
\ No newline at end of file
diff --git a/src/web/urls.py b/src/web/urls.py
index 788be59bb3..ceb24fa423 100755
--- a/src/web/urls.py
+++ b/src/web/urls.py
@@ -37,7 +37,6 @@ urlpatterns = [
# Admin interface
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
- url(r'^admin/', include(admin.site.urls)),
# favicon
url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')),
@@ -46,7 +45,22 @@ urlpatterns = [
url(r'^webclient/', include('src.web.webclient.urls', namespace='webclient', app_name='webclient')),
# Front page
- url(r'^$', 'src.web.views.page_index', name="index")]
+ url(r'^$', 'src.web.views.page_index', name="index"),
+
+ # Django original admin page. Make this URL is always available, whether
+ # we've chosen to use Evennia's custom admin or not.
+ url(r'django_admin/', 'src.web.views.admin_wrapper', name="django_admin")]
+
+if settings.EVENNIA_ADMIN:
+ urlpatterns += [
+ # Our override for the admin.
+ url('^admin/$', 'src.web.views.evennia_admin', name="evennia_admin"),
+
+ # Makes sure that other admin pages get loaded.
+ url(r'^admin/', include(admin.site.urls))]
+else:
+ # Just include the normal Django admin.
+ urlpatterns += [url(r'^admin/', include(admin.site.urls))]
# This sets up the server if the user want to run the Django
# test server (this should normally not be needed).
diff --git a/src/web/views.py b/src/web/views.py
index 2126a74c55..986ec141d8 100644
--- a/src/web/views.py
+++ b/src/web/views.py
@@ -5,18 +5,18 @@ the other applications. Views are django's way of processing e.g. html
templates on the fly.
"""
-from django.shortcuts import render_to_response
-from django.template import RequestContext
-#from django.contrib.auth.models import User
+from django.contrib.admin.sites import site
from django.conf import settings
+from django.contrib.admin.views.decorators import staff_member_required
+from django.shortcuts import render
from src.objects.models import ObjectDB
-#from src.typeclasses.models import TypedObject
from src.players.models import PlayerDB
from src.web.news.models import NewsEntry
_BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
+
def page_index(request):
"""
Main root page.
@@ -56,8 +56,8 @@ def page_index(request):
"num_others": nothers or "no"
}
- context_instance = RequestContext(request)
- return render_to_response('index.html', pagevars, context_instance)
+ return render(request, 'index.html', pagevars)
+
def to_be_implemented(request):
"""
@@ -69,7 +69,21 @@ def to_be_implemented(request):
"page_title": "To Be Implemented...",
}
- context_instance = RequestContext(request)
- return render_to_response('tbi.html', pagevars, context_instance)
+ return render(request, 'tbi.html', pagevars)
+@staff_member_required
+def evennia_admin(request):
+ """
+ Helpful Evennia-specific admin page.
+ """
+ return render(
+ request, 'evennia_admin.html', {
+ 'playerdb': PlayerDB})
+
+
+def admin_wrapper(request):
+ """
+ Wrapper that allows us to properly use the base Django admin site, if needed.
+ """
+ return staff_member_required(site.index)(request)
\ No newline at end of file
From fa20190467f9f01ed6c7012d51508a6ec335bfaf Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Thu, 5 Jun 2014 19:17:10 -0500
Subject: [PATCH 17/25] Added collectstatic to start/reload.
---
game/evennia.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/game/evennia.py b/game/evennia.py
index d1ed65fd69..998b078cb5 100755
--- a/game/evennia.py
+++ b/game/evennia.py
@@ -16,6 +16,8 @@ from optparse import OptionParser
from subprocess import Popen
# Set the Python path up so we can get to settings.py from here.
+from django.core import management
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
@@ -264,6 +266,8 @@ def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART
return
os.remove(pidfile)
# set restart/norestart flag
+ if restart == 'reload':
+ management.call_command('collectstatic', interactive=False, verbosity=0)
f = open(restart_file, 'w')
f.write(str(restart))
f.close()
@@ -389,11 +393,13 @@ def handle_args(options, mode, service):
if inter:
cmdstr.append('--iportal')
cmdstr.append('--noserver')
+ management.call_command('collectstatic', verbosity=1, interactive=False)
else: # all
# for convenience we don't start logging of
# portal, only of server with this command.
if inter:
cmdstr.extend(['--iserver'])
+ management.call_command('collectstatic', verbosity=1, interactive=False)
return cmdstr
elif mode == 'reload':
@@ -425,6 +431,7 @@ def handle_args(options, mode, service):
kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown")
return None
+
def error_check_python_modules():
"""
Import settings modules in settings. This will raise exceptions on
@@ -509,7 +516,7 @@ def main():
if mode not in ['menu', 'start', 'reload', 'stop']:
print "mode should be none, 'menu', 'start', 'reload' or 'stop'."
sys.exit()
- if service not in ['server', 'portal', 'all']:
+ if service not in ['server', 'portal', 'all']:
print "service should be none, 'server', 'portal' or 'all'."
sys.exit()
From ca3f92acd0031a656da70c98dc8f69b8cbb1f460 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sat, 28 Jun 2014 16:16:09 -0500
Subject: [PATCH 18/25] Admin interface greatly improved. Support for editing
Attributes added.
Resolves #503. Resolves #201.
---
src/comms/admin.py | 10 ++
src/objects/admin.py | 22 +---
src/players/admin.py | 219 ++++++++++++++++++++++++---------------
src/scripts/admin.py | 14 +--
src/typeclasses/admin.py | 64 ++++++++++++
src/utils/picklefield.py | 49 ++++++++-
6 files changed, 268 insertions(+), 110 deletions(-)
create mode 100644 src/typeclasses/admin.py
diff --git a/src/comms/admin.py b/src/comms/admin.py
index 2fd5da1fa3..5a8c790c99 100644
--- a/src/comms/admin.py
+++ b/src/comms/admin.py
@@ -5,6 +5,15 @@
from django.contrib import admin
from src.comms.models import ChannelDB
+from src.typeclasses.admin import AttributeInline, TagInline
+
+
+class ChannelAttributeInline(AttributeInline):
+ model = ChannelDB.db_attributes.through
+
+
+class ChannelTagInline(TagInline):
+ model = ChannelDB.db_tags.through
class MsgAdmin(admin.ModelAdmin):
@@ -21,6 +30,7 @@ class MsgAdmin(admin.ModelAdmin):
class ChannelAdmin(admin.ModelAdmin):
+ inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions")
list_display_links = ("id", 'db_key')
ordering = ["db_key"]
diff --git a/src/objects/admin.py b/src/objects/admin.py
index 3d4adeb6a1..78d7838c4f 100644
--- a/src/objects/admin.py
+++ b/src/objects/admin.py
@@ -6,27 +6,16 @@
from django import forms
from django.conf import settings
from django.contrib import admin
-from src.typeclasses.models import Attribute, Tag
+from src.typeclasses.admin import AttributeInline, TagInline
from src.objects.models import ObjectDB
-class AttributeInline(admin.TabularInline):
- # This class is currently not used, because PickleField objects are
- # not editable. It's here for us to ponder making a way that allows
- # them to be edited.
- model = Attribute
- fields = ('db_key', 'db_value')
- extra = 0
+class ObjectAttributeInline(AttributeInline):
+ model = ObjectDB.db_attributes.through
-class TagInline(admin.TabularInline):
+class ObjectTagInline(TagInline):
model = ObjectDB.db_tags.through
- raw_id_fields = ('tag',)
- extra = 0
-
-
-class TagAdmin(admin.ModelAdmin):
- fields = ('db_key', 'db_category', 'db_data')
class ObjectCreateForm(forms.ModelForm):
@@ -59,6 +48,7 @@ class ObjectEditForm(ObjectCreateForm):
class ObjectDBAdmin(admin.ModelAdmin):
+ inlines = [ObjectTagInline, ObjectAttributeInline]
list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path')
list_display_links = ('id', 'db_key')
ordering = ['db_player', 'db_typeclass_path', 'id']
@@ -88,7 +78,6 @@ class ObjectDBAdmin(admin.ModelAdmin):
# )
#deactivated temporarily, they cause empty objects to be created in admin
- inlines = [TagInline]
# Custom modification to give two different forms wether adding or not.
add_form = ObjectCreateForm
@@ -135,4 +124,3 @@ class ObjectDBAdmin(admin.ModelAdmin):
admin.site.register(ObjectDB, ObjectDBAdmin)
-admin.site.register(Tag, TagAdmin)
diff --git a/src/players/admin.py b/src/players/admin.py
index 279d1fff01..4dd89e684d 100644
--- a/src/players/admin.py
+++ b/src/players/admin.py
@@ -4,15 +4,12 @@
#
from django import forms
-#from django.db import models
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
-#from django.contrib.admin import widgets
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
-#from django.contrib.auth.models import User
from src.players.models import PlayerDB
-#from src.typeclasses.models import Attribute
+from src.typeclasses.admin import AttributeInline, TagInline
from src.utils import create
@@ -22,19 +19,25 @@ class PlayerDBChangeForm(UserChangeForm):
class Meta:
model = PlayerDB
- username = forms.RegexField(label="Username",
- max_length=30,
- regex=r'^[\w. @+-]+$',
- widget=forms.TextInput(attrs={'size':'30'}),
- error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."},
- help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.")
+ username = forms.RegexField(
+ label="Username",
+ max_length=30,
+ regex=r'^[\w. @+-]+$',
+ widget=forms.TextInput(
+ attrs={'size': '30'}),
+ error_messages={
+ 'invalid': "This value may contain only letters, spaces, numbers "
+ "and @/./+/-/_ characters."},
+ help_text="30 characters or fewer. Letters, spaces, digits and "
+ "@/./+/-/_ only.")
def clean_username(self):
username = self.cleaned_data['username']
if username.upper() == self.instance.username.upper():
return username
elif PlayerDB.objects.filter(username__iexact=username):
- raise forms.ValidationError('A player with that name already exists.')
+ raise forms.ValidationError('A player with that name '
+ 'already exists.')
return self.cleaned_data['username']
@@ -43,75 +46,90 @@ class PlayerDBCreationForm(UserCreationForm):
class Meta:
model = PlayerDB
- username = forms.RegexField(label="Username",
- max_length=30,
- regex=r'^[\w. @+-]+$',
- widget=forms.TextInput(attrs={'size':'30'}),
- error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."},
- help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.")
+ username = forms.RegexField(
+ label="Username",
+ max_length=30,
+ regex=r'^[\w. @+-]+$',
+ widget=forms.TextInput(
+ attrs={'size': '30'}),
+ error_messages={
+ 'invalid': "This value may contain only letters, spaces, numbers "
+ "and @/./+/-/_ characters."},
+ help_text="30 characters or fewer. Letters, spaces, digits and "
+ "@/./+/-/_ only.")
def clean_username(self):
username = self.cleaned_data['username']
if PlayerDB.objects.filter(username__iexact=username):
- raise forms.ValidationError('A player with that name already exists.')
+ raise forms.ValidationError('A player with that name already '
+ 'exists.')
return username
-# # The Player editor
-# class AttributeForm(forms.ModelForm):
-# "Defines how to display the atttributes"
-# class Meta:
-# model = Attribute
-# db_key = forms.CharField(label="Key",
-# widget=forms.TextInput(attrs={'size':'15'}))
-# db_value = forms.CharField(label="Value",
-# widget=forms.Textarea(attrs={'rows':'2'}))
-
-# class AttributeInline(admin.TabularInline):
-# "Inline creation of player attributes"
-# model = Attribute
-# extra = 0
-# form = AttributeForm
-# fieldsets = (
-# (None, {'fields' : (('db_key', 'db_value'))}),)
-
class PlayerForm(forms.ModelForm):
- "Defines how to display Players"
-
+ """
+ Defines how to display Players
+ """
class Meta:
model = PlayerDB
- db_key = forms.RegexField(label="Username",
- initial="PlayerDummy",
- max_length=30,
- regex=r'^[\w. @+-]+$',
- required=False,
- widget=forms.TextInput(attrs={'size':'30'}),
- error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."},
- help_text = "This should be the same as the connected Player's key name. 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.")
+ db_key = forms.RegexField(
+ label="Username",
+ initial="PlayerDummy",
+ max_length=30,
+ regex=r'^[\w. @+-]+$',
+ required=False,
+ widget=forms.TextInput(attrs={'size': '30'}),
+ error_messages={
+ 'invalid': "This value may contain only letters, spaces, numbers"
+ " and @/./+/-/_ characters."},
+ help_text="This should be the same as the connected Player's key "
+ "name. 30 characters or fewer. Letters, spaces, digits and "
+ "@/./+/-/_ only.")
- db_typeclass_path = forms.CharField(label="Typeclass",
- initial=settings.BASE_PLAYER_TYPECLASS,
- widget=forms.TextInput(attrs={'size':'78'}),
- help_text="Required. Defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. Defaults to settings.BASE_PLAYER_TYPECLASS.")
- #db_permissions = forms.CharField(label="Permissions",
- # initial=settings.PERMISSION_PLAYER_DEFAULT,
- # required=False,
- # widget=forms.TextInput(attrs={'size':'78'}),
- # help_text="In-game permissions. A comma-separated list of text strings checked by certain locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. A Player permission can be overloaded by the permissions of a controlled Character. Normal players use 'Players' by default.")
- db_lock_storage = forms.CharField(label="Locks",
- widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}),
- required=False,
- help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form type:lockfunction(args);type2:lockfunction2(args);...")
- db_cmdset_storage = forms.CharField(label="cmdset",
- initial=settings.CMDSET_PLAYER,
- widget=forms.TextInput(attrs={'size':'78'}),
- required=False,
- help_text="python path to player cmdset class (set in settings.CMDSET_PLAYER by default)")
+ db_typeclass_path = forms.CharField(
+ label="Typeclass",
+ initial=settings.BASE_PLAYER_TYPECLASS,
+ widget=forms.TextInput(
+ attrs={'size': '78'}),
+ help_text="Required. Defines what 'type' of entity this is. This "
+ "variable holds a Python path to a module with a valid "
+ "Evennia Typeclass. Defaults to "
+ "settings.BASE_PLAYER_TYPECLASS.")
+
+ db_permissions = forms.CharField(
+ label="Permissions",
+ initial=settings.PERMISSION_PLAYER_DEFAULT,
+ required=False,
+ widget=forms.TextInput(
+ attrs={'size': '78'}),
+ help_text="In-game permissions. A comma-separated list of text "
+ "strings checked by certain locks. They are often used for "
+ "hierarchies, such as letting a Player have permission "
+ "'Wizards', 'Builders' etc. A Player permission can be "
+ "overloaded by the permissions of a controlled Character. "
+ "Normal players use 'Players' by default.")
+
+ db_lock_storage = forms.CharField(
+ label="Locks",
+ widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}),
+ required=False,
+ help_text="In-game lock definition string. If not given, defaults "
+ "will be used. This string should be on the form "
+ "type:lockfunction(args);type2:lockfunction2(args);...")
+ db_cmdset_storage = forms.CharField(
+ label="cmdset",
+ initial=settings.CMDSET_PLAYER,
+ widget=forms.TextInput(attrs={'size': '78'}),
+ required=False,
+ help_text="python path to player cmdset class (set in "
+ "settings.CMDSET_PLAYER by default)")
class PlayerInline(admin.StackedInline):
- "Inline creation of Player"
+ """
+ Inline creation of Player
+ """
model = PlayerDB
template = "admin/players/stacked.html"
form = PlayerForm
@@ -119,51 +137,80 @@ class PlayerInline(admin.StackedInline):
("In-game Permissions and Locks",
{'fields': ('db_lock_storage',),
#{'fields': ('db_permissions', 'db_lock_storage'),
- 'description':"These are permissions/locks for in-game use. They are unrelated to website access rights."}),
+ 'description': "These are permissions/locks for in-game use. "
+ "They are unrelated to website access rights."}),
("In-game Player data",
- {'fields':('db_typeclass_path', 'db_cmdset_storage'),
- 'description':"These fields define in-game-specific properties for the Player object in-game."}),
- )
+ {'fields': ('db_typeclass_path', 'db_cmdset_storage'),
+ 'description': "These fields define in-game-specific properties "
+ "for the Player object in-game."}))
extra = 1
max_num = 1
+class PlayerTagInline(TagInline):
+ model = PlayerDB.db_tags.through
+
+
+class PlayerAttributeInline(AttributeInline):
+ model = PlayerDB.db_attributes.through
+
+
class PlayerDBAdmin(BaseUserAdmin):
- "This is the main creation screen for Users/players"
+ """
+ This is the main creation screen for Users/players
+ """
list_display = ('username', 'email', 'is_staff', 'is_superuser')
form = PlayerDBChangeForm
add_form = PlayerDBCreationForm
+ inlines = [PlayerTagInline, PlayerAttributeInline]
fieldsets = (
(None, {'fields': ('username', 'password', 'email')}),
- ('Website profile', {'fields': ('first_name', 'last_name'),
- 'description': "These are not used in the default system."}),
- ('Website dates', {'fields': ('last_login', 'date_joined'),
- 'description': 'Relevant only to the website.'}),
- ('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',
- 'user_permissions', 'groups'),
- 'description': "These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights."}),
- ('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'),
- 'description': 'These are attributes that are more relevant to gameplay.'}))
- #('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'),
- # 'description': 'These are attributes that are more relevant to gameplay.'}))
+ ('Website profile', {
+ 'fields': ('first_name', 'last_name'),
+ 'description': "These are not used "
+ "in the default system."}),
+ ('Website dates', {
+ 'fields': ('last_login', 'date_joined'),
+ 'description': 'Relevant only to the website.'}),
+ ('Website Permissions', {
+ 'fields': ('is_active', 'is_staff', 'is_superuser',
+ 'user_permissions', 'groups'),
+ 'description': "These are permissions/permission groups for "
+ "accessing the admin site. They are unrelated to "
+ "in-game access rights."}),
+ ('Game Options', {
+ 'fields': ('db_typeclass_path', 'db_cmdset_storage',
+ 'db_lock_storage'),
+ 'description': 'These are attributes that are more relevant '
+ 'to gameplay.'}))
+ # ('Game Options', {'fields': (
+ # 'db_typeclass_path', 'db_cmdset_storage',
+ # 'db_permissions', 'db_lock_storage'),
+ # 'description': 'These are attributes that are '
+ # 'more relevant to gameplay.'}))
add_fieldsets = (
(None,
{'fields': ('username', 'password1', 'password2', 'email'),
- 'description':"These account details are shared by the admin system and the game."},),)
+ 'description': "These account details are shared by the admin "
+ "system and the game."},),)
# TODO! Remove User reference!
def save_formset(self, request, form, formset, change):
- "Run all hooks on the player object"
+ """
+ Run all hooks on the player object
+ """
super(PlayerDBAdmin, self).save_formset(request, form, formset, change)
userobj = form.instance
userobj.name = userobj.username
if not change:
- #uname, passwd, email = str(request.POST.get(u"username")), \
- # str(request.POST.get(u"password1")), str(request.POST.get(u"email"))
- typeclass = str(request.POST.get(u"playerdb_set-0-db_typeclass_path"))
+ # uname, passwd, email = str(request.POST.get(u"username")), \
+ # str(request.POST.get(u"password1")), \
+ # str(request.POST.get(u"email"))
+ typeclass = str(request.POST.get(
+ u"playerdb_set-0-db_typeclass_path"))
create.create_player("", "", "",
user=userobj,
typeclass=typeclass,
diff --git a/src/scripts/admin.py b/src/scripts/admin.py
index 13ff1cbdf7..76e2562d88 100644
--- a/src/scripts/admin.py
+++ b/src/scripts/admin.py
@@ -2,16 +2,18 @@
# This sets up how models are displayed
# in the web admin interface.
#
+from src.typeclasses.admin import AttributeInline, TagInline
-from src.typeclasses.models import Attribute
from src.scripts.models import ScriptDB
from django.contrib import admin
-class AttributeInline(admin.TabularInline):
- model = Attribute
- fields = ('db_key', 'db_value')
- max_num = 1
+class ScriptTagInline(TagInline):
+ model = ScriptDB.db_tags.through
+
+
+class ScriptAttributeInline(AttributeInline):
+ model = ScriptDB.db_attributes.through
class ScriptDBAdmin(admin.ModelAdmin):
@@ -32,7 +34,7 @@ class ScriptDBAdmin(admin.ModelAdmin):
'db_repeats', 'db_start_delay', 'db_persistent',
'db_obj')}),
)
- #inlines = [AttributeInline]
+ inlines = [ScriptTagInline, ScriptAttributeInline]
admin.site.register(ScriptDB, ScriptDBAdmin)
diff --git a/src/typeclasses/admin.py b/src/typeclasses/admin.py
new file mode 100644
index 0000000000..72d8e90235
--- /dev/null
+++ b/src/typeclasses/admin.py
@@ -0,0 +1,64 @@
+from django.contrib import admin
+from django.contrib.admin import ModelAdmin
+from django.core.urlresolvers import reverse
+from django.forms import Textarea
+from src.typeclasses.models import Attribute, Tag
+
+
+class PickledWidget(Textarea):
+ pass
+
+
+class TagAdmin(admin.ModelAdmin):
+ fields = ('db_key', 'db_category', 'db_data')
+
+
+class TagInline(admin.TabularInline):
+ # Set this to the through model of your desired M2M when subclassing.
+ model = None
+ raw_id_fields = ('tag',)
+ extra = 0
+
+
+class AttributeInline(admin.TabularInline):
+ """
+ Inline creation of player attributes
+ """
+ # Set this to the through model of your desired M2M when subclassing.
+ model = None
+ extra = 3
+ #form = AttributeForm
+ fields = ('attribute', 'key', 'value', 'strvalue')
+ raw_id_fields = ('attribute',)
+ readonly_fields = ('key', 'value', 'strvalue')
+
+ def key(self, instance):
+ if not instance.id:
+ return "Not yet set or saved."
+ return '%s' % (
+ reverse("admin:typeclasses_attribute_change",
+ args=[instance.attribute.id]),
+ instance.attribute.db_key)
+
+ key.allow_tags = True
+
+ def value(self, instance):
+ if not instance.id:
+ return "Not yet set or saved."
+ return instance.attribute.db_value
+
+ def strvalue(self, instance):
+ if not instance.id:
+ return "Not yet set or saved."
+ return instance.attribute.db_strvalue
+
+
+class AttributeAdmin(ModelAdmin):
+ """
+ Defines how to display the attributes
+ """
+ search_fields = ('db_key', 'db_strvalue', 'db_value')
+ list_display = ('db_key', 'db_strvalue', 'db_value')
+
+admin.site.register(Attribute, AttributeAdmin)
+admin.site.register(Tag, TagAdmin)
\ No newline at end of file
diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py
index dc33464ee3..d7b2d409cd 100644
--- a/src/utils/picklefield.py
+++ b/src/utils/picklefield.py
@@ -28,15 +28,21 @@ Pickle field implementation for Django.
Modified for Evennia by Griatch.
"""
+from ast import literal_eval
from copy import deepcopy
from base64 import b64encode, b64decode
from zlib import compress, decompress
#import six # this is actually a pypy component, not in default syslib
import django
+from django.core.exceptions import ValidationError
from django.db import models
# django 1.5 introduces force_text instead of force_unicode
+from django.forms import CharField, Textarea
+from django.forms.util import flatatt
+from django.utils.html import format_html
+
try:
from django.utils.encoding import force_text
except ImportError:
@@ -120,6 +126,45 @@ def _get_subfield_superclass():
#return six.with_metaclass(models.SubfieldBase, models.Field)
+class PickledWidget(Textarea):
+ def render(self, name, value, attrs=None):
+ value = repr(value)
+ try:
+ literal_eval(value)
+ except ValueError:
+ return value
+
+ final_attrs = self.build_attrs(attrs, name=name)
+ return format_html('',
+ flatatt(final_attrs),
+ force_text(value))
+
+
+class PickledFormField(CharField):
+ widget = PickledWidget
+ default_error_messages = dict(CharField.default_error_messages)
+ default_error_messages['invalid'] = (
+ "This is not a Python Literal. You can store things like strings, "
+ "integers, or floats, but you must do it by typing them as you would "
+ "type them in the Python Interpreter. For instance, strings must be "
+ "surrounded by quote marks. We have converted it to a string for your "
+ "convenience. If it is acceptable, please hit save again.")
+
+ def __init__(self, *args, **kwargs):
+ # This needs to fall through to literal_eval.
+ kwargs['required'] = False
+ super(PickledFormField, self).__init__(*args, **kwargs)
+
+ def clean(self, value):
+ if value == '':
+ # Field was left blank. Make this None.
+ value = 'None'
+ try:
+ return literal_eval(value)
+ except ValueError:
+ raise ValidationError(self.error_messages['invalid'])
+
+
class PickledObjectField(_get_subfield_superclass()):
"""
A field that will accept *any* python object and store it in the
@@ -135,7 +180,6 @@ class PickledObjectField(_get_subfield_superclass()):
def __init__(self, *args, **kwargs):
self.compress = kwargs.pop('compress', False)
self.protocol = kwargs.pop('protocol', DEFAULT_PROTOCOL)
- kwargs.setdefault('editable', False)
super(PickledObjectField, self).__init__(*args, **kwargs)
def get_default(self):
@@ -180,6 +224,9 @@ class PickledObjectField(_get_subfield_superclass()):
return value._obj
return value
+ def formfield(self, **kwargs):
+ return PickledFormField(**kwargs)
+
def pre_save(self, model_instance, add):
value = super(PickledObjectField, self).pre_save(model_instance, add)
return wrap_conflictual_object(value)
From a6187ed997ff41bb7ca86658a401133a5704268a Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sat, 28 Jun 2014 16:35:57 -0500
Subject: [PATCH 19/25] Minor cleanup in a readme and a method signature.
---
game/gamesrc/web/static/README.md | 10 ++++++----
src/web/webclient/views.py | 8 +++-----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/game/gamesrc/web/static/README.md b/game/gamesrc/web/static/README.md
index 8ba3d8ff20..7b76fffe1f 100644
--- a/game/gamesrc/web/static/README.md
+++ b/game/gamesrc/web/static/README.md
@@ -1,5 +1,7 @@
-This folder is used by Django's staticfiles application for gathering all static files (CSS, JS, etc) into one place
-from the various sources that they might be gathered from.
+If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, copy it into this folder, and it will be placed in the static folder when you run:
-Do not edit files in this directory. Instead, read up on Django's static file handling to learn how to create Django
-apps with their own static files.
\ No newline at end of file
+ python manage.py collectstatic
+
+or reload the server via the command line.
+
+Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place.
diff --git a/src/web/webclient/views.py b/src/web/webclient/views.py
index d74ff4f5e2..c8ea3c82ab 100644
--- a/src/web/webclient/views.py
+++ b/src/web/webclient/views.py
@@ -4,12 +4,11 @@ This contains a simple view for rendering the webclient
page and serve it eventual static content.
"""
+from django.shortcuts import render
-from django.shortcuts import render_to_response, redirect
-from django.template import RequestContext
-from django.conf import settings
from src.players.models import PlayerDB
+
def webclient(request):
"""
Webclient page template loading.
@@ -25,5 +24,4 @@ def webclient(request):
# as an example we send the number of connected players to the template
pagevars = {'num_players_connected': nsess}
- context_instance = RequestContext(request)
- return render_to_response('webclient.html', pagevars, context_instance)
+ return render(request, 'webclient.html', pagevars)
From a1b596a8479321c189979172a16c28159486ee9d Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sat, 28 Jun 2014 18:01:00 -0500
Subject: [PATCH 20/25] Fixed an issue where saving an attribute/tag would make
duplicates.
---
src/typeclasses/admin.py | 7 +----
src/typeclasses/models.py | 56 +++++++++++++++++++++++++++------------
src/utils/picklefield.py | 2 +-
3 files changed, 41 insertions(+), 24 deletions(-)
diff --git a/src/typeclasses/admin.py b/src/typeclasses/admin.py
index 72d8e90235..6ec88623e5 100644
--- a/src/typeclasses/admin.py
+++ b/src/typeclasses/admin.py
@@ -1,14 +1,9 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.core.urlresolvers import reverse
-from django.forms import Textarea
from src.typeclasses.models import Attribute, Tag
-class PickledWidget(Textarea):
- pass
-
-
class TagAdmin(admin.ModelAdmin):
fields = ('db_key', 'db_category', 'db_data')
@@ -26,7 +21,7 @@ class AttributeInline(admin.TabularInline):
"""
# Set this to the through model of your desired M2M when subclassing.
model = None
- extra = 3
+ extra = 1
#form = AttributeForm
fields = ('attribute', 'key', 'value', 'strvalue')
raw_id_fields = ('attribute',)
diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py
index 5abea472e9..774ef451bb 100644
--- a/src/typeclasses/models.py
+++ b/src/typeclasses/models.py
@@ -34,6 +34,7 @@ import weakref
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
+from django.db.models import Q
from django.utils.encoding import smart_str
from django.contrib.contenttypes.models import ContentType
@@ -46,7 +47,8 @@ from src.server.models import ServerConfig
from src.typeclasses import managers
from src.locks.lockhandler import LockHandler
from src.utils import logger
-from src.utils.utils import make_iter, is_iter, to_str, inherits_from, LazyLoadHandler
+from src.utils.utils import (
+ make_iter, is_iter, to_str, inherits_from, LazyLoadHandler)
from src.utils.dbserialize import to_pickle, from_pickle
from src.utils.picklefield import PickledObjectField
@@ -69,7 +71,6 @@ _DA = object.__delattr__
#
#------------------------------------------------------------
-#class Attribute(SharedMemoryModel):
class Attribute(SharedMemoryModel):
"""
Abstract django model.
@@ -99,20 +100,33 @@ class Attribute(SharedMemoryModel):
# These database fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix.
db_key = models.CharField('key', max_length=255, db_index=True)
- # access through the value property
- db_value = PickledObjectField('value', null=True)
- # string-specific storage for quick look-up
- db_strvalue = models.TextField('strvalue', null=True, blank=True)
- # optional categorization of attribute
- db_category = models.CharField('category', max_length=128, db_index=True, blank=True, null=True)
+ db_value = PickledObjectField(
+ 'value', null=True,
+ help_text="The data returned when the attribute is accessed. Must be "
+ "written as a Python literal if editing through the admin "
+ "interface.")
+ db_strvalue = models.TextField(
+ 'strvalue', null=True, blank=True,
+ help_text="String-specific storage for quick look-up")
+ db_category = models.CharField(
+ 'category', max_length=128, db_index=True, blank=True, null=True,
+ help_text="Optional categorization of attribute.")
# Lock storage
- db_lock_storage = models.TextField('locks', blank=True)
- # Which model of object this Attribute is attached to (A natural key like objects.dbobject)
- db_model = models.CharField('model', max_length=32, db_index=True, blank=True, null=True)
+ db_lock_storage = models.TextField(
+ 'locks', blank=True,
+ help_text="Lockstrings for this object are stored here.")
+ db_model = models.CharField(
+ 'model', max_length=32, db_index=True, blank=True, null=True,
+ help_text="Which model of object this attribute is attached to (A "
+ "natural key like objects.dbobject). You should not change "
+ "this value unless you know what you are doing.")
# subclass of Attribute (None or nick)
- db_attrtype = models.CharField('attrtype', max_length=16, db_index=True, blank=True, null=True)
+ db_attrtype = models.CharField(
+ 'attrtype', max_length=16, db_index=True, blank=True, null=True,
+ help_text="Subclass of Attribute (None or nick)")
# time stamp
- db_date_created = models.DateTimeField('date_created', editable=False, auto_now_add=True)
+ db_date_created = models.DateTimeField(
+ 'date_created', editable=False, auto_now_add=True)
# Database manager
objects = managers.AttributeManager()
@@ -226,10 +240,14 @@ class AttributeHandler(object):
self._cache = None
def _recache(self):
+ if not self._attrtype:
+ attrtype = Q(db_attrtype=None) | Q(db_attrtype='')
+ else:
+ attrtype = Q(db_attrtype=self._attrtype)
self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(),
attr.db_category.lower() if attr.db_category else None), attr)
for attr in getattr(self.obj, self._m2m_fieldname).filter(
- db_model=self._model, db_attrtype=self._attrtype))
+ db_model=self._model).filter(attrtype))
#set_attr_cache(self.obj, self._cache) # currently only for testing
def has(self, key, category=None):
@@ -551,12 +569,16 @@ class TagHandler(object):
self._model = "%s.%s" % ContentType.objects.get_for_model(obj).natural_key()
self._cache = None
-
def _recache(self):
"Update cache from database field"
+ if not self._tagtype:
+ tagtype = Q(db_tagtype='') | Q(db_tagtype__isnull=True)
+ else:
+ tagtype = Q(db_tagtype=self._tagtype)
self._cache = dict(("%s-%s" % (tag.db_key, tag.db_category), tag)
- for tag in getattr(self.obj, self._m2m_fieldname).filter(
- db_model=self._model, db_tagtype=self._tagtype))
+ for tag in getattr(
+ self.obj, self._m2m_fieldname).filter(
+ db_model=self._model).filter(tagtype))
def add(self, tag, category=None, data=None):
"Add a new tag to the handler. Tag is a string or a list of strings."
diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py
index d7b2d409cd..3c59ad31f9 100644
--- a/src/utils/picklefield.py
+++ b/src/utils/picklefield.py
@@ -161,7 +161,7 @@ class PickledFormField(CharField):
value = 'None'
try:
return literal_eval(value)
- except ValueError:
+ except (ValueError, SyntaxError):
raise ValidationError(self.error_messages['invalid'])
From a34ddea236b69164bf63e8602417e5e26249978f Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 29 Jun 2014 07:05:45 -0500
Subject: [PATCH 21/25] Fixed placement of websocket javascript and template.
Fixed wrong readme in static.
Moved backends.py into web/util
---
game/gamesrc/web/static/README.md | 8 ++------
game/gamesrc/web/static_overrides/README.md | 2 ++
src/settings_default.py | 3 ++-
src/web/{ => utils}/backends.py | 0
.../static/webclient/js}/evennia_websocket_webclient.js | 0
src/web/webclient/templates/webclient.html | 4 ++--
6 files changed, 8 insertions(+), 9 deletions(-)
rename src/web/{ => utils}/backends.py (100%)
rename src/web/{media/javascript => webclient/static/webclient/js}/evennia_websocket_webclient.js (100%)
diff --git a/game/gamesrc/web/static/README.md b/game/gamesrc/web/static/README.md
index 7b76fffe1f..10e1c27214 100644
--- a/game/gamesrc/web/static/README.md
+++ b/game/gamesrc/web/static/README.md
@@ -1,7 +1,3 @@
-If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, copy it into this folder, and it will be placed in the static folder when you run:
+DO NOT EDIT FILES IN THIS DIRECTORY! THEY WILL BE OVERWRITTEN.
- python manage.py collectstatic
-
-or reload the server via the command line.
-
-Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place.
+If you need to edit static files, see the static_overrides directory.
diff --git a/game/gamesrc/web/static_overrides/README.md b/game/gamesrc/web/static_overrides/README.md
index 8640413887..0bc428b141 100644
--- a/game/gamesrc/web/static_overrides/README.md
+++ b/game/gamesrc/web/static_overrides/README.md
@@ -2,4 +2,6 @@ If you want to override one of the static files (such as a CSS or JS file) used
python manage.py collectstatic
+...or when you reload the server via the command line.
+
Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place.
diff --git a/src/settings_default.py b/src/settings_default.py
index d819a3b8cd..e64eaab545 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -113,7 +113,8 @@ CYCLE_LOGFILES = True
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
TIME_ZONE = 'UTC'
# Authentication backends. This is the code used to authenticate a user.
-AUTHENTICATION_BACKENDS = ('src.web.backends.CaseInsensitiveModelBackend',)
+AUTHENTICATION_BACKENDS = (
+ 'src.web.utils.backends.CaseInsensitiveModelBackend',)
# Language code for this installation. All choices can be found here:
# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
LANGUAGE_CODE = 'en-us'
diff --git a/src/web/backends.py b/src/web/utils/backends.py
similarity index 100%
rename from src/web/backends.py
rename to src/web/utils/backends.py
diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js
similarity index 100%
rename from src/web/media/javascript/evennia_websocket_webclient.js
rename to src/web/webclient/static/webclient/js/evennia_websocket_webclient.js
diff --git a/src/web/webclient/templates/webclient.html b/src/web/webclient/templates/webclient.html
index efc1c90dfc..b28aa204cd 100644
--- a/src/web/webclient/templates/webclient.html
+++ b/src/web/webclient/templates/webclient.html
@@ -21,14 +21,14 @@
if ("WebSocket" in window) {
var wsurl = "{{websocket_url}}";
- document.write("\")}
+ document.write("\")}
else {
document.write("\")}
{% else %}
-
+
{% endif %}
From def97b58a89992a5208f05cd7da4b20aa9813fdb Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 29 Jun 2014 08:14:33 -0500
Subject: [PATCH 22/25] Renamed static ev directory to evennia_general
Removed defunct news app.
---
src/settings_default.py | 1 -
src/web/news/__init__.py | 0
src/web/news/admin.py | 17 ---
src/web/news/models.py | 47 -------
src/web/news/urls.py | 12 --
src/web/news/views.py | 129 ------------------
.../css/prosimii-print.css | 0
.../css/prosimii-screen-alt.css | 0
.../css/prosimii-screen.css | 0
.../{ev => evennia_general}/images/LICENCE | 0
.../images/evennia_logo.png | Bin
.../images/evennia_logo_small.png | Bin
.../images/favicon.ico | Bin
src/web/templates/prosimii/base.html | 10 +-
src/web/templates/prosimii/news/archive.html | 51 -------
.../templates/prosimii/news/search_form.html | 19 ---
.../templates/prosimii/news/show_entry.html | 14 --
src/web/urls.py | 3 -
18 files changed, 5 insertions(+), 298 deletions(-)
delete mode 100755 src/web/news/__init__.py
delete mode 100644 src/web/news/admin.py
delete mode 100755 src/web/news/models.py
delete mode 100755 src/web/news/urls.py
delete mode 100755 src/web/news/views.py
rename src/web/static/{ev => evennia_general}/css/prosimii-print.css (100%)
rename src/web/static/{ev => evennia_general}/css/prosimii-screen-alt.css (100%)
rename src/web/static/{ev => evennia_general}/css/prosimii-screen.css (100%)
rename src/web/static/{ev => evennia_general}/images/LICENCE (100%)
rename src/web/static/{ev => evennia_general}/images/evennia_logo.png (100%)
rename src/web/static/{ev => evennia_general}/images/evennia_logo_small.png (100%)
rename src/web/static/{ev => evennia_general}/images/favicon.ico (100%)
delete mode 100644 src/web/templates/prosimii/news/archive.html
delete mode 100644 src/web/templates/prosimii/news/search_form.html
delete mode 100644 src/web/templates/prosimii/news/show_entry.html
diff --git a/src/settings_default.py b/src/settings_default.py
index e64eaab545..e0db9a45af 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -569,7 +569,6 @@ INSTALLED_APPS = (
'src.comms',
'src.help',
'src.scripts',
- 'src.web.news',
'src.web.webclient')
# The user profile extends the User object with more functionality;
# This should usually not be changed.
diff --git a/src/web/news/__init__.py b/src/web/news/__init__.py
deleted file mode 100755
index e69de29bb2..0000000000
diff --git a/src/web/news/admin.py b/src/web/news/admin.py
deleted file mode 100644
index 9e48748f01..0000000000
--- a/src/web/news/admin.py
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# This makes the news model visible in the admin web interface
-# so one can add/edit/delete news items etc.
-#
-
-from django.contrib import admin
-from src.web.news.models import NewsTopic, NewsEntry
-
-class NewsTopicAdmin(admin.ModelAdmin):
- list_display = ('name', 'icon')
-admin.site.register(NewsTopic, NewsTopicAdmin)
-
-class NewsEntryAdmin(admin.ModelAdmin):
- list_display = ('title', 'author', 'topic', 'date_posted')
- list_filter = ('topic',)
- search_fields = ['title']
-admin.site.register(NewsEntry, NewsEntryAdmin)
diff --git a/src/web/news/models.py b/src/web/news/models.py
deleted file mode 100755
index d89d3362df..0000000000
--- a/src/web/news/models.py
+++ /dev/null
@@ -1,47 +0,0 @@
-#
-# This module implements a simple news entry system
-# for the evennia website. One needs to use the
-# admin interface to add/edit/delete entries.
-#
-
-from django.db import models
-from django.contrib.auth import get_user_model
-
-User = get_user_model()
-
-class NewsTopic(models.Model):
- """
- Represents a news topic.
- """
- name = models.CharField(max_length=75, unique=True)
- description = models.TextField(blank=True)
- icon = models.ImageField(upload_to='newstopic_icons',
- default='newstopic_icons/default.png',
- blank=True, help_text="Image for the news topic.")
-
- def __str__(self):
- try:
- return self.name
- except:
- return "Invalid"
-
- class Meta:
- ordering = ['name']
-
-class NewsEntry(models.Model):
- """
- An individual news entry.
- """
- author = models.ForeignKey(User, related_name='author')
- title = models.CharField(max_length=255)
- body = models.TextField()
- topic = models.ForeignKey(NewsTopic, related_name='newstopic')
- date_posted = models.DateTimeField(auto_now_add=True)
-
- def __str__(self):
- return self.title
-
- class Meta:
- ordering = ('-date_posted',)
- verbose_name_plural = "News entries"
-
diff --git a/src/web/news/urls.py b/src/web/news/urls.py
deleted file mode 100755
index d0a769531f..0000000000
--- a/src/web/news/urls.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-This structures the url tree for the news application.
-It is imported from the root handler, game.web.urls.py.
-"""
-
-from django.conf.urls import *
-
-urlpatterns = [
- url(r'^show/(?P\d+)/$', 'show_news', name="show"),
- url(r'^archive/$', 'news_archive', name="archive"),
- url(r'^search/$', 'search_form', name="search"),
- url(r'^search/results/$', 'search_results', name="search_results")]
diff --git a/src/web/news/views.py b/src/web/news/views.py
deleted file mode 100755
index 54f2caabab..0000000000
--- a/src/web/news/views.py
+++ /dev/null
@@ -1,129 +0,0 @@
-
-"""
-This is a very simple news application, with most of the expected features
-like news-categories/topics and searchable archives.
-
-"""
-
-from django.views.generic import ListView
-from django.shortcuts import render_to_response, get_object_or_404
-from django.template import RequestContext
-from django.conf import settings
-from django.http import HttpResponseRedirect
-from django.contrib.auth.models import User
-from django import forms
-from django.db.models import Q
-
-from src.web.news.models import NewsTopic, NewsEntry
-
-# The sidebar text to be included as a variable on each page. There's got to
-# be a better, cleaner way to include this on every page.
-sidebar = """
-
-"""
-
-class SearchForm(forms.Form):
- """
- Class to represent a news search form under Django's newforms. This is used
- to validate the input on the search_form view, as well as the search_results
- view when we're picking the query out of GET. This makes searching safe
- via the search form or by directly inputing values via GET key pairs.
- """
- search_terms = forms.CharField(max_length=100, min_length=3, required=True)
-
-def show_news(request, entry_id):
- """
- Show an individual news entry. Display some basic information along with
- the title and content.
- """
- news_entry = get_object_or_404(NewsEntry, id=entry_id)
-
- pagevars = {
- "page_title": "News Entry",
- "news_entry": news_entry,
- "sidebar": sidebar
- }
-
- context_instance = RequestContext(request)
- return render_to_response('news/show_entry.html', pagevars, context_instance)
-
-def news_archive(request):
- """
- Shows an archive of news entries.
-
- TODO: Expand this a bit to allow filtering by month/year.
- """
- news_entries = NewsEntry.objects.all().order_by('-date_posted')
- # TODO: Move this to either settings.py or the SQL configuration.
- entries_per_page = 15
-
- pagevars = {
- "page_title": "News Archive",
- "browse_url": "/news/archive",
- "sidebar": sidebar
- }
- view = ListView.as_view(queryset=news_entries)
- return view(request, template_name='news/archive.html', \
- extra_context=pagevars, paginate_by=entries_per_page)
-
-def search_form(request):
- """
- Render the news search form. Don't handle much validation at all. If the
- user enters a search term that meets the minimum, send them on their way
- to the results page.
- """
- if request.method == 'GET':
- # A GET request was sent to the search page, load the value and
- # validate it.
- search_form = SearchForm(request.GET)
- if search_form.is_valid():
- # If the input is good, send them to the results page with the
- # query attached in GET variables.
- return HttpResponseRedirect('/news/search/results/?search_terms='+ search_form.cleaned_data['search_terms'])
- else:
- # Brand new search, nothing has been sent just yet.
- search_form = SearchForm()
-
- pagevars = {
- "page_title": "Search News",
- "search_form": search_form,
- "debug": settings.DEBUG,
- "sidebar": sidebar
- }
-
- context_instance = RequestContext(request)
- return render_to_response('news/search_form.html', pagevars, context_instance)
-
-def search_results(request):
- """
- Shows an archive of news entries. Use the generic news browsing template.
- """
- # TODO: Move this to either settings.py or the SQL configuration.
- entries_per_page = 15
-
- # Load the form values from GET to validate against.
- search_form = SearchForm(request.GET)
- # You have to call is_valid() or cleaned_data won't be populated.
- valid_search = search_form.is_valid()
- # This is the safe data that we can pass to queries without huge worry of
- # badStuff(tm).
- cleaned_get = search_form.cleaned_data
-
- # Perform searches that match the title and contents.
- # TODO: Allow the user to specify what to match against and in what
- # topics/categories.
- news_entries = NewsEntry.objects.filter(Q(title__contains=cleaned_get['search_terms']) | Q(body__contains=cleaned_get['search_terms']))
-
- pagevars = {
- "game_name": settings.SERVERNAME,
- "page_title": "Search Results",
- "searchtext": cleaned_get['search_terms'],
- "browse_url": "/news/search/results",
- "sidebar": sidebar
- }
- view = ListView.as_view(queryset=news_entries)
- return view(request, news_entries, template_name='news/archive.html', extra_context=pagevars, paginate_by=entries_per_page)
diff --git a/src/web/static/ev/css/prosimii-print.css b/src/web/static/evennia_general/css/prosimii-print.css
similarity index 100%
rename from src/web/static/ev/css/prosimii-print.css
rename to src/web/static/evennia_general/css/prosimii-print.css
diff --git a/src/web/static/ev/css/prosimii-screen-alt.css b/src/web/static/evennia_general/css/prosimii-screen-alt.css
similarity index 100%
rename from src/web/static/ev/css/prosimii-screen-alt.css
rename to src/web/static/evennia_general/css/prosimii-screen-alt.css
diff --git a/src/web/static/ev/css/prosimii-screen.css b/src/web/static/evennia_general/css/prosimii-screen.css
similarity index 100%
rename from src/web/static/ev/css/prosimii-screen.css
rename to src/web/static/evennia_general/css/prosimii-screen.css
diff --git a/src/web/static/ev/images/LICENCE b/src/web/static/evennia_general/images/LICENCE
similarity index 100%
rename from src/web/static/ev/images/LICENCE
rename to src/web/static/evennia_general/images/LICENCE
diff --git a/src/web/static/ev/images/evennia_logo.png b/src/web/static/evennia_general/images/evennia_logo.png
similarity index 100%
rename from src/web/static/ev/images/evennia_logo.png
rename to src/web/static/evennia_general/images/evennia_logo.png
diff --git a/src/web/static/ev/images/evennia_logo_small.png b/src/web/static/evennia_general/images/evennia_logo_small.png
similarity index 100%
rename from src/web/static/ev/images/evennia_logo_small.png
rename to src/web/static/evennia_general/images/evennia_logo_small.png
diff --git a/src/web/static/ev/images/favicon.ico b/src/web/static/evennia_general/images/favicon.ico
similarity index 100%
rename from src/web/static/ev/images/favicon.ico
rename to src/web/static/evennia_general/images/favicon.ico
diff --git a/src/web/templates/prosimii/base.html b/src/web/templates/prosimii/base.html
index a0008f2e63..4c260f8c2e 100644
--- a/src/web/templates/prosimii/base.html
+++ b/src/web/templates/prosimii/base.html
@@ -9,12 +9,12 @@
{% if sidebar %}
-
+
{% else %}
-
+
{% endif %}
-
-
+
+
{% block header_ext %}
{% endblock %}
@@ -35,7 +35,7 @@
Enter a search term or phrase to search by. Matches will be made against
- news titles and their contents. Searches must be at least three characters
- long.
By {{news_entry.author.username}} on {{news_entry.date_posted|time}}
-
{{news_entry.body}}
-{% endblock %}
diff --git a/src/web/urls.py b/src/web/urls.py
index ceb24fa423..ea9e1576bc 100755
--- a/src/web/urls.py
+++ b/src/web/urls.py
@@ -29,9 +29,6 @@ urlpatterns = [
url(r'^accounts/login', 'django.contrib.auth.views.login', name="login"),
url(r'^accounts/logout', 'django.contrib.auth.views.logout', name="logout"),
- # News stuff
- # url(r'^news/', include('src.web.news.urls')),
-
# Page place-holder for things that aren't implemented yet.
url(r'^tbi/', 'src.web.views.to_be_implemented', name='to_be_implemented'),
From 7499932337fd314ca1cea92e6f111d4616c90f03 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 29 Jun 2014 08:23:12 -0500
Subject: [PATCH 23/25] Removed creation of admin media symlink. Fixed media
handling.
---
game/gamesrc/web/media/README.md | 1 +
src/server/initial_setup.py | 37 --------------------------------
src/settings_default.py | 9 ++------
3 files changed, 3 insertions(+), 44 deletions(-)
create mode 100644 game/gamesrc/web/media/README.md
diff --git a/game/gamesrc/web/media/README.md b/game/gamesrc/web/media/README.md
new file mode 100644
index 0000000000..5f2c3da7a8
--- /dev/null
+++ b/game/gamesrc/web/media/README.md
@@ -0,0 +1 @@
+This directory is where file uploads from Django apps are placed by default.
\ No newline at end of file
diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py
index 031abf56a8..a7af784edd 100644
--- a/src/server/initial_setup.py
+++ b/src/server/initial_setup.py
@@ -180,42 +180,6 @@ def start_game_time():
gametime.init_gametime()
-def create_admin_media_links():
- """
- This traverses to src/web/media and tries to create a symbolic
- link to the django media files from within the MEDIA_ROOT.
- These are files we normally don't
- want to mess with (use templates to customize the admin
- look). Linking is needed since the Twisted webserver otherwise has no
- notion of where the default files are - and we cannot hard-code it
- since the django install may be at different locations depending
- on system.
- """
- import django
- import os
-
- if django.get_version() < 1.4:
- dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
- else:
- dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'static', 'admin')
- apath = os.path.join(settings.ADMIN_MEDIA_ROOT)
- if os.path.isdir(apath):
- print " ADMIN_MEDIA_ROOT already exists. Ignored."
- return
- if os.name == 'nt':
- print " Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode)."
- os.mkdir(apath)
- os.system('xcopy "%s" "%s" /e /q /c' % (dpath, apath))
- if os.name == 'posix':
- try:
- os.symlink(dpath, apath)
- print " Admin-media symlinked to ADMIN_MEDIA_ROOT."
- except OSError, e:
- print " There was an error symlinking Admin-media to ADMIN_MEDIA_ROOT:\n %s\n -> \n %s\n (%s)\n If you see issues, link manually." % (dpath, apath, e)
- else:
- print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT."
-
-
def at_initial_setup():
"""
Custom hook for users to overload some or all parts of the initial
@@ -269,7 +233,6 @@ def handle_setup(last_step):
create_channels,
create_system_scripts,
start_game_time,
- create_admin_media_links,
at_initial_setup,
reset_server
]
diff --git a/src/settings_default.py b/src/settings_default.py
index e0db9a45af..76dc73ad0e 100644
--- a/src/settings_default.py
+++ b/src/settings_default.py
@@ -457,14 +457,9 @@ TEMPLATE_DEBUG = DEBUG
ADMINS = () #'Your Name', 'your_email@domain.com'),)
# These guys get broken link notifications when SEND_BROKEN_LINK_EMAILS is True.
MANAGERS = ADMINS
-# Absolute path to the directory that holds media (no trailing slash).
+# Absolute path to the directory that holds file uploads from web apps.
# Example: "/home/media/media.lawrence.com"
-MEDIA_ROOT = os.path.join(SRC_DIR, 'web', 'media')
-# Absolute path to the directory that holds (usually links to) the
-# django admin media files. If the target directory does not exist, it
-# is created and linked by Evennia upon first start. Otherwise link it
-# manually to django/contrib/admin/media.
-ADMIN_MEDIA_ROOT = os.path.join(MEDIA_ROOT, 'admin')
+MEDIA_ROOT = os.path.join(GAME_DIR, "gamesrc", "web", "media")
# It's safe to dis-regard this, as it's a Django feature we only half use as a
# dependency, not actually what it's primarily meant for.
SITE_ID = 1
From 112e7c652d35faa4bcbdca0d976733d69696208f Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 29 Jun 2014 08:29:41 -0500
Subject: [PATCH 24/25] Leftover news app cleanup, .gitignore updated.
---
.gitignore | 1 +
src/web/templates/prosimii/index.html | 15 ---------------
src/web/views.py | 4 ----
3 files changed, 1 insertion(+), 19 deletions(-)
diff --git a/.gitignore b/.gitignore
index 413011b83d..93408a842a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ __pycache__
game/settings.py
game/logs/*.log.*
game/gamesrc/web/static/*
+game/gamesrc/web/media/*
# Installer logs
pip-log.txt
diff --git a/src/web/templates/prosimii/index.html b/src/web/templates/prosimii/index.html
index 19e69d8238..2c7aa752a4 100644
--- a/src/web/templates/prosimii/index.html
+++ b/src/web/templates/prosimii/index.html
@@ -28,21 +28,6 @@
mailing list or to come say hi in the developer chatroom. If you find bugs, please report them to our Issue tracker.
-
-
-
-
-
diff --git a/src/web/views.py b/src/web/views.py
index 986ec141d8..078af83045 100644
--- a/src/web/views.py
+++ b/src/web/views.py
@@ -12,7 +12,6 @@ from django.shortcuts import render
from src.objects.models import ObjectDB
from src.players.models import PlayerDB
-from src.web.news.models import NewsEntry
_BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
@@ -26,8 +25,6 @@ def page_index(request):
fpage_player_limit = 4
fpage_news_entries = 2
- # A QuerySet of recent news entries.
- news_entries = NewsEntry.objects.all().order_by('-date_posted')[:fpage_news_entries]
# A QuerySet of the most recently connected players.
recent_users = PlayerDB.objects.get_recently_connected_players()[:fpage_player_limit]
nplyrs_conn_recent = len(recent_users) or "none"
@@ -43,7 +40,6 @@ def page_index(request):
pagevars = {
"page_title": "Front Page",
- "news_entries": news_entries,
"players_connected_recent": recent_users,
"num_players_connected": nsess or "noone",
"num_players_registered": nplyrs or "no",
From 54bb593f5e973d1841bd36cd726dcd500b558756 Mon Sep 17 00:00:00 2001
From: Kelketek Rritaa
Date: Sun, 29 Jun 2014 09:14:01 -0500
Subject: [PATCH 25/25] Prevent editing of attributes which are not Python
literals through the admin interface.
---
src/typeclasses/admin.py | 9 +++++++++
src/typeclasses/models.py | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/typeclasses/admin.py b/src/typeclasses/admin.py
index 6ec88623e5..525d702f1c 100644
--- a/src/typeclasses/admin.py
+++ b/src/typeclasses/admin.py
@@ -54,6 +54,15 @@ class AttributeAdmin(ModelAdmin):
"""
search_fields = ('db_key', 'db_strvalue', 'db_value')
list_display = ('db_key', 'db_strvalue', 'db_value')
+ permitted_types = ('str', 'int', 'float', 'NoneType', 'bool')
+
+ fields = ('db_key', 'db_value', 'db_strvalue', 'db_category',
+ 'db_lock_storage', 'db_model', 'db_attrtype')
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj.db_value.__class__.__name__ not in self.permitted_types:
+ return ['db_value']
+ return []
admin.site.register(Attribute, AttributeAdmin)
admin.site.register(Tag, TagAdmin)
\ No newline at end of file
diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py
index 774ef451bb..2d96558fae 100644
--- a/src/typeclasses/models.py
+++ b/src/typeclasses/models.py
@@ -104,7 +104,8 @@ class Attribute(SharedMemoryModel):
'value', null=True,
help_text="The data returned when the attribute is accessed. Must be "
"written as a Python literal if editing through the admin "
- "interface.")
+ "interface. Attribute values which are not Python literals "
+ "cannot be edited through the admin interface.")
db_strvalue = models.TextField(
'strvalue', null=True, blank=True,
help_text="String-specific storage for quick look-up")