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).

@@ -49,7 +49,7 @@   {% endif %} - {% endif %} + {% endif %} {% endfor %} @@ -58,15 +58,15 @@ {% endif %} {% endfor %} -

Game entities

+

Game entities

- {% 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.

+ effect of some kind.

{% endif %}
@@ -99,18 +99,18 @@ {% endif %} {% endfor %} - - -

Website

- {% for app in app_list %} +

Website

+ + + {% for app in app_list %} {% if app.name in evennia_websiteapps %} - {% if app.name == 'Flatpages' %} + {% if app.name == 'Flatpages' %}

Miscellaneous objects related to the running and - managing of the Web presence.

+ managing of the Web presence.

{% endif %}
diff --git a/src/web/templates/prosimii/webclient.html b/src/web/templates/prosimii/webclient.html index 313736329e..b0d815170f 100644 --- a/src/web/templates/prosimii/webclient.html +++ b/src/web/templates/prosimii/webclient.html @@ -9,13 +9,26 @@ - + + - + + + {% if websocket_enabled %} + + {% else %} + + + {% endif %} - - @@ -38,7 +51,7 @@
Logged in Players: {{num_players_connected}}
-
+
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 @@
Logged in Players: {{num_players_connected}}
-
+
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 %} 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)

+ (Back) {% endblock %} {% block nav-global %}{% endblock %} diff --git a/src/web/templates/prosimii/base.html b/src/web/templates/prosimii/base.html index 76e241dc3f..a0008f2e63 100644 --- a/src/web/templates/prosimii/base.html +++ b/src/web/templates/prosimii/base.html @@ -1,3 +1,4 @@ +{% load staticfiles %} @@ -8,12 +9,12 @@ {% if sidebar %} - + {% else %} - + {% endif %} - - + + {% block header_ext %} {% endblock %} @@ -34,7 +35,7 @@
-

{{game_name}}

+

{{game_name}}

{{game_slogan}}   @@ -45,13 +46,13 @@
@@ -61,9 +62,9 @@ Home | About | Documentation | - Admin Interface + Admin Interface {% if webclient_enabled %} - | Play Online + | Play Online {% endif %}
@@ -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 %} {% 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)

-{% endblock %} - -{% block nav-global %}{% endblock %} diff --git a/src/web/templates/admin/index.html b/src/web/templates/admin/index.html deleted file mode 100644 index 7a4f0a5295..0000000000 --- a/src/web/templates/admin/index.html +++ /dev/null @@ -1,184 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_static %} - -{% block extrastyle %}{{ block.super }}{% endblock %} - -{% block coltype %}colMS{% endblock %} - -{% block bodyclass %}dashboard{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -
- -{% 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).

- {% endif %} - -
- - - {% for model in app.models %} - - {% if model.name == "Players" %} - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% if model.perms.add %} - - {% else %} - - {% endif %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% endif %} - - {% endfor %} -
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
PlayerPlayer{% trans 'Add' %} {% trans 'Change' %} 
-
- - {% endif %} - {% endfor %} - -

Game entities

- - {% for app in app_list %} - - {% if app.name in evennia_entityapps %} - - {% if app.name == 'Comms' %} -

This defines entities that has an in-game precense or - effect of some kind.

- {% endif %} - -
- - - {% for model in app.models %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% if model.perms.add %} - - {% else %} - - {% endif %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - - {% endfor %} -
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
-
- - {% endif %} - {% endfor %} - - -

Website

- - - {% for app in app_list %} - - {% if app.name in evennia_websiteapps %} - - {% if app.name == 'Flatpages' %} -

Miscellaneous objects related to the running and - managing of the Web presence.

- {% endif %} - -
- - - {% for model in app.models %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - {% if model.perms.add %} - - {% else %} - - {% endif %} - - {% if model.perms.change %} - - {% else %} - - {% endif %} - - - {% endfor %} -
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
-
- - {% endif %} - {% endfor %} - - -{% else %} -

{% trans "You don't have permission to edit anything." %}

-{% endif %} -
-{% endblock %} - -{% block sidebar %} - -{% endblock %} diff --git a/src/web/templates/admin/players/add_form.html b/src/web/templates/admin/players/add_form.html deleted file mode 100644 index 85cdb8739b..0000000000 --- a/src/web/templates/admin/players/add_form.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "admin/players/change_form.html" %} -{% load i18n %} - -{% block form_top %} - {% if not is_popup %} -

{% trans "First, enter a username and password. Then you'll be able to edit more Player options." %}

- {% else %} -

{% trans "Enter a username and password." %}

- {% endif %} -{% endblock %} - -{% block after_field_sets %} - -{% endblock %} diff --git a/src/web/templates/admin/players/change_form.html b/src/web/templates/admin/players/change_form.html deleted file mode 100644 index 405f73b916..0000000000 --- a/src/web/templates/admin/players/change_form.html +++ /dev/null @@ -1,70 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_modify admin_static %} - -{% block extrahead %}{{ block.super }} -{% url 'admin:jsi18n' as jsi18nurl %} - -{{ media }} -{% endblock %} - -{% block extrastyle %}{{ block.super }}{% endblock %} - -{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %} - -{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} -change-form{% endblock %} - -{% block breadcrumbs %}{% if not is_popup %} - -{% endif %}{% endblock %} -{% block content %}
-{% block object-tools %} -{% if change %}{% if not is_popup %} - -{% endif %}{% endif %} -{% endblock %} -
{% csrf_token %}{% block form_top %}{% endblock %} -
-{% if is_popup %}{% endif %} -{% if save_on_top %}{% submit_row %}{% endif %} -{% if errors %} -

- {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} -

- {{ adminform.form.non_field_errors }} -{% endif %} - -{% for fieldset in adminform %} - {% include "admin/includes/fieldset.html" %} -{% endfor %} - -{% block after_field_sets %}{% endblock %} - -{% for inline_admin_formset in inline_admin_formsets %} - {% include inline_admin_formset.opts.template %} -{% endfor %} - -{% block after_related_objects %}{% endblock %} - -{% submit_row %} - -{% if adminform and add %} - -{% endif %} - -{# JavaScript for prepopulated fields #} -{% prepopulated_fields_js %} - -
-
-{% endblock %} diff --git a/src/web/templates/admin/players/change_list.html b/src/web/templates/admin/players/change_list.html deleted file mode 100644 index 15c655089a..0000000000 --- a/src/web/templates/admin/players/change_list.html +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load admin_static admin_list i18n %} -{% block extrastyle %} - {{ block.super }} - - {% if cl.formset %} - - {% endif %} - {% if cl.formset or action_form %} - {% url 'admin:jsi18n' as jsi18nurl %} - - {% endif %} - {{ media.css }} - {% if not actions_on_top and not actions_on_bottom %} - - {% endif %} -{% endblock %} - -{% block extrahead %} -{{ block.super }} -{{ media.js }} -{% if action_form %}{% if actions_on_top or actions_on_bottom %} - -{% endif %}{% endif %} -{% endblock %} - -{% block bodyclass %}change-list{% endblock %} - -{% if not is_popup %} - {% block breadcrumbs %} - - {% endblock %} -{% endif %} - -{% block coltype %}flex{% endblock %} - -{% block content %} -
- {% block object-tools %} - {% if has_add_permission %} - - {% endif %} - {% endblock %} - {% if cl.formset.errors %} -

- {% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} -

- {{ cl.formset.non_form_errors }} - {% endif %} -
- {% block search %}{% search_form cl %}{% endblock %} - {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %} - - {% block filters %} - {% if cl.has_filters %} -
-

{% trans 'Filter' %}

- {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} -
- {% endif %} - {% endblock %} - -
{% csrf_token %} - {% if cl.formset %} -
{{ cl.formset.management_form }}
- {% endif %} - - {% block result_list %} - {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} - {% result_list cl %} - {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} - {% endblock %} - {% block pagination %}{% pagination cl %}{% endblock %} -
-
-
-{% endblock %} diff --git a/src/web/templates/admin/players/stacked.html b/src/web/templates/admin/players/stacked.html deleted file mode 100644 index 617ca9987e..0000000000 --- a/src/web/templates/admin/players/stacked.html +++ /dev/null @@ -1,82 +0,0 @@ -{% load i18n admin_static %} -
- -{{ inline_admin_formset.formset.management_form }} -{{ inline_admin_formset.formset.non_form_errors }} - -{% for inline_admin_form in inline_admin_formset %}
- - {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} - {% for fieldset in inline_admin_form %} - {% include "admin/includes/fieldset.html" %} - {% endfor %} - {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} - {{ inline_admin_form.fk_field.field }} -
{% endfor %} -
- - diff --git a/src/web/templates/prosimii/evennia_admin.html b/src/web/templates/prosimii/evennia_admin.html new file mode 100644 index 0000000000..ab7b3a42a2 --- /dev/null +++ b/src/web/templates/prosimii/evennia_admin.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block content %} +

Admin

+ Welcome to the Evennia Admin Page. Here, you can edit many facets of players, characters, and other parts of the game. + +

Players

+ Players are user accounts. Players can have several characters under them. A user's login and password information can be changed here. + +

Objects

+ Objects include everything from characters to rooms to exits. + +

Scripts

+ Scripts are meta objects used to store game information, handle special functionality or perform timed actions. + +

Channels

+ Channels are used for player communications. + +

Help Topics

+ +

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('\r\n{1}', + 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("\ {% 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 = """ -

This page’s menu:

- -""" - -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 @@
-

{{game_name}}

+

{{game_name}}

{{game_slogan}}   diff --git a/src/web/templates/prosimii/news/archive.html b/src/web/templates/prosimii/news/archive.html deleted file mode 100644 index e3859771ec..0000000000 --- a/src/web/templates/prosimii/news/archive.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "base.html" %} - -{% block header_ext %} -{% endblock %} - -{% block sidebar %} -{{sidebar|safe}} -{% endblock %} - -{% block content %} -

{{page_title}}

- -Navigation: First | -{% if has_previous %} - Prev -{% else %} - Prev -{% endif %} - -| {{page}} of {{pages}} pages | - -{% if has_next %} - Next -{% else %} - Next -{% endif %} -| Last - -{% for entry in object_list %} - {{entry.topic.name}}: {{entry.title}} -

By {{entry.author.username}} on {{entry.date_posted|time}}

-

{{entry.body|truncatewords:80}}

-{% endfor %} - -Navigation: First | -{% if has_previous %} - Prev -{% else %} - Prev -{% endif %} - -| {{page}} of {{pages}} pages | - -{% if has_next %} - Next -{% else %} - Next -{% endif %} -| Last - -{% endblock %} diff --git a/src/web/templates/prosimii/news/search_form.html b/src/web/templates/prosimii/news/search_form.html deleted file mode 100644 index 940bb0a09a..0000000000 --- a/src/web/templates/prosimii/news/search_form.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} - -{% block header_ext %} -{% endblock %} - -{% block sidebar %} -{{sidebar|safe}} -{% endblock %} - -{% block content %} -

Search News

-

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.

-
- {{search_form.search_terms}} - -
-{% endblock %} diff --git a/src/web/templates/prosimii/news/show_entry.html b/src/web/templates/prosimii/news/show_entry.html deleted file mode 100644 index 475d087016..0000000000 --- a/src/web/templates/prosimii/news/show_entry.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block header_ext %} -{% endblock %} - -{% block sidebar %} -{{sidebar|safe}} -{% endblock %} - -{% block content %} -

{{news_entry.topic.name}}: {{news_entry.title}}

-

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")