Merge branch 'master' into develop

This commit is contained in:
Griatch 2017-10-05 23:18:22 +02:00
commit 2585d33e7c
9 changed files with 131 additions and 57 deletions

View file

@ -2324,6 +2324,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
Note that the only way to retrieve
an object from a None location is by direct #dbref
reference. A puppeted object cannot be moved to None.
loc - teleport object to the target's location instead of its contents
Teleports an object somewhere. If no object is given, you yourself
is teleported to the target location. """
@ -2343,6 +2344,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
# setting switches
tel_quietly = "quiet" in switches
to_none = "tonone" in switches
to_loc = "loc" in switches
if to_none:
# teleporting to None
@ -2368,7 +2370,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
# not teleporting to None location
if not args and not to_none:
caller.msg("Usage: teleport[/switches] [<obj> =] <target_loc>|home")
caller.msg("Usage: teleport[/switches] [<obj> =] <target_loc>||home")
return
if rhs:
@ -2384,6 +2386,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
if not destination:
caller.msg("Destination not found.")
return
if to_loc:
destination = destination.location
if not destination:
caller.msg("Destination has no location.")
return
if obj_to_teleport == destination:
caller.msg("You can't teleport an object inside of itself!")
return

View file

@ -17,6 +17,7 @@ from evennia.utils.utils import string_suggestions, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
HELP_MORE = settings.HELP_MORE
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
# limit symbol import for API
__all__ = ("CmdHelp", "CmdSetHelp")
@ -231,6 +232,15 @@ class CmdHelp(Command):
# try an exact command auto-help match
match = [cmd for cmd in all_cmds if cmd == query]
if not match:
# try an inexact match with prefixes stripped from query and cmds
_query = query[1:] if query[0] in CMD_IGNORE_PREFIXES else query
match = [cmd for cmd in all_cmds
for m in cmd._matchset if m == _query or
m[0] in CMD_IGNORE_PREFIXES and m[1:] == _query]
if len(match) == 1:
formatted = self.format_help_entry(match[0].key,
match[0].get_help(caller, cmdset),

View file

@ -293,9 +293,9 @@ if WEBSERVER_ENABLED:
# create ajax client processes at /webclientdata
from evennia.server.portal import webclient_ajax
webclient = webclient_ajax.WebClient()
webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", webclient)
ajax_webclient = webclient_ajax.AjaxWebClient()
ajax_webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", ajax_webclient)
webclientstr = "\n + webclient (ajax only)"
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:

View file

@ -93,7 +93,6 @@ class WebSocketClient(Protocol, Session):
csession = self.get_client_session()
if csession:
print("In disconnect: csession uid=%s" % csession.get("webclient_authenticated_uid", None))
csession["webclient_authenticated_uid"] = None
csession.save()
self.logged_in = False

View file

@ -53,11 +53,11 @@ def jsonify(obj):
#
# WebClient resource - this is called by the ajax client
# AjaxWebClient resource - this is called by the ajax client
# using POST requests to /webclientdata.
#
class WebClient(resource.Resource):
class AjaxWebClient(resource.Resource):
"""
An ajax/comet long-polling transport
@ -163,13 +163,13 @@ class WebClient(resource.Resource):
remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port)
sess = WebClientSession()
sess = AjaxWebClientSession()
sess.client = self
sess.init_session("ajax/comet", remote_addr, self.sessionhandler)
sess.csessid = csessid
csession = _CLIENT_SESSIONS(session_key=sess.csessid)
uid = csession and csession.get("logged_in", False)
uid = csession and csession.get("webclient_authenticated_uid", False)
if uid:
# the client session is already logged in
sess.uid = uid
@ -292,14 +292,26 @@ class WebClient(resource.Resource):
# web client interface.
#
class WebClientSession(session.Session):
class AjaxWebClientSession(session.Session):
"""
This represents a session running in a webclient.
This represents a session running in an AjaxWebclient.
"""
def __init__(self, *args, **kwargs):
self.protocol_name = "ajax/comet"
super(WebClientSession, self).__init__(*args, **kwargs)
super(AjaxWebClientSession, self).__init__(*args, **kwargs)
def get_client_session(self):
"""
Get the Client browser session (used for auto-login based on browser session)
Returns:
csession (ClientSession): This is a django-specific internal representation
of the browser session.
"""
if self.csessid:
return _CLIENT_SESSIONS(session_key=self.csessid)
def disconnect(self, reason="Server disconnected."):
"""
@ -308,10 +320,22 @@ class WebClientSession(session.Session):
Args:
reason (str): Motivation for the disconnect.
"""
csession = self.get_client_session()
if csession:
csession["webclient_authenticated_uid"] = None
csession.save()
self.logged_in = False
self.client.lineSend(self.csessid, ["connection_close", [reason], {}])
self.client.client_disconnect(self.csessid)
self.sessionhandler.disconnect(self)
def at_login(self):
csession = self.get_client_session()
if csession:
csession["webclient_authenticated_uid"] = self.uid
csession.save()
def data_out(self, **kwargs):
"""
Data Evennia -> User

View file

@ -18,17 +18,28 @@ class TagAdmin(admin.ModelAdmin):
class TagForm(forms.ModelForm):
"""
This form overrides the base behavior of the ModelForm that would be used for a Tag-through-model.
Since the through-models only have access to the foreignkeys of the Tag and the Object that they're
attached to, we need to spoof the behavior of it being a form that would correspond to its tag,
or the creation of a tag. Instead of being saved, we'll call to the Object's handler, which will handle
the creation, change, or deletion of a tag for us, as well as updating the handler's cache so that all
changes are instantly updated in-game.
This form overrides the base behavior of the ModelForm that would be used for a
Tag-through-model. Since the through-models only have access to the foreignkeys of the Tag and
the Object that they're attached to, we need to spoof the behavior of it being a form that would
correspond to its tag, or the creation of a tag. Instead of being saved, we'll call to the
Object's handler, which will handle the creation, change, or deletion of a tag for us, as well
as updating the handler's cache so that all changes are instantly updated in-game.
"""
tag_key = forms.CharField(label='Tag Name')
tag_category = forms.CharField(label="Category", required=False)
tag_type = forms.CharField(label="Type", required=False)
tag_data = forms.CharField(label="Data", required=False)
tag_key = forms.CharField(label='Tag Name',
required=True,
help_text="This is the main key identifier")
tag_category = forms.CharField(label="Category",
help_text="Used for grouping tags. Unset (default) gives a category of None",
required=False)
tag_type = forms.CharField(label="Type",
help_text="Internal use. Either unset, \"alias\" or \"permission\"",
required=False)
tag_data = forms.CharField(label="Data",
help_text="Usually unused. Intended for eventual info about the tag itself",
required=False)
class Meta:
fields = ("tag_key", "tag_category", "tag_data", "tag_type")
def __init__(self, *args, **kwargs):
"""
@ -121,8 +132,8 @@ class TagInline(admin.TabularInline):
form = TagForm
formset = TagFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
raw_id_fields = ('tag',)
readonly_fields = ('tag',)
# raw_id_fields = ('tag',)
# readonly_fields = ('tag',)
extra = 0
def get_formset(self, request, obj=None, **kwargs):
@ -150,13 +161,26 @@ class AttributeForm(forms.ModelForm):
changes are instantly updated in-game.
"""
attr_key = forms.CharField(label='Attribute Name', required=False, initial="Enter Attribute Name Here")
attr_category = forms.CharField(label="Category", help_text="type of attribute, for sorting", required=False)
attr_category = forms.CharField(label="Category",
help_text="type of attribute, for sorting",
required=False,
max_length=4)
attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False)
attr_type = forms.CharField(label="Type", help_text="nick for nickname, else leave blank", required=False)
attr_type = forms.CharField(label="Type",
help_text="Internal use. Either unset (normal Attribute) or \"nick\"",
required=False,
max_length=4)
attr_strvalue = forms.CharField(label="String Value",
help_text="Only enter this if value is blank and you want to save as a string",
required=False)
attr_lockstring = forms.CharField(label="Locks", required=False, widget=forms.Textarea)
help_text="Only set when using the Attribute as a string-only store",
required=False,
widget=forms.Textarea(attrs={"rows": 1, "cols": 6}))
attr_lockstring = forms.CharField(label="Locks",
required=False,
help_text="Lock string on the form locktype:lockdef;lockfunc:lockdef;...",
widget=forms.Textarea(attrs={"rows": 1, "cols": 8}))
class Meta:
fields = ("attr_key", "attr_value", "attr_category", "attr_strvalue", "attr_lockstring", "attr_type")
def __init__(self, *args, **kwargs):
"""
@ -164,6 +188,7 @@ class AttributeForm(forms.ModelForm):
to have based on the Attribute. attr_key, attr_category, attr_value, attr_strvalue, attr_type,
and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
similarly be populated.
"""
super(AttributeForm, self).__init__(*args, **kwargs)
attr_key = None
@ -261,8 +286,8 @@ class AttributeInline(admin.TabularInline):
form = AttributeForm
formset = AttributeFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
raw_id_fields = ('attribute',)
readonly_fields = ('attribute',)
# raw_id_fields = ('attribute',)
# readonly_fields = ('attribute',)
extra = 0
def get_formset(self, request, obj=None, **kwargs):

View file

@ -126,7 +126,13 @@ class PickledWidget(Textarea):
except ValueError:
return value
final_attrs = self.build_attrs(attrs, name=name)
# fix since the signature of build_attrs changed in Django 1.11
if attrs is not None:
attrs["name"] = name
else:
attrs = {"name": name}
final_attrs = self.build_attrs(attrs)
return format_html('<textarea{0}>\r\n{1}</textarea>',
flatatt(final_attrs),
value)

View file

@ -10,12 +10,12 @@ old and does not support websockets, it will instead fall back to a
long-polling (AJAX/COMET) type of connection (using
evennia/server/portal/webclient_ajax.py)
All messages is a valid JSON array on single form:
All messages are valid JSON arrays on this single form:
["cmdname", args, kwargs],
where args is an JSON array and kwargs is a JSON object that will be
used as argument to call the cmdname function.
where args is an JSON array and kwargs is a JSON object. These will be both
used as arguments emitted to a callback named "cmdname" as cmdname(args, kwargs).
This library makes the "Evennia" object available. It has the
following official functions:
@ -45,7 +45,7 @@ An "emitter" object must have a function
relay the data to its correct gui element.
- The default emitter also has the following methods:
- on(cmdname, listener) - this ties a listener to the backend. This function
should be called as listener(kwargs) when the backend calls emit.
should be called as listener(args, kwargs) when the backend calls emit.
- off(cmdname) - remove the listener for this cmdname.
*/

View file

@ -369,28 +369,29 @@ function onNewLine(text, originator) {
unread++;
favico.badge(unread);
document.title = "(" + unread + ") " + originalTitle;
if ("Notification" in window){
if (("notification_popup" in options) && (options["notification_popup"])) {
Notification.requestPermission().then(function(result) {
if(result === "granted") {
var title = originalTitle === "" ? "Evennia" : originalTitle;
var options = {
body: text.replace(/(<([^>]+)>)/ig,""),
icon: "/static/website/images/evennia_logo.png"
}
if (("notification_popup" in options) && (options["notification_popup"])) {
Notification.requestPermission().then(function(result) {
if(result === "granted") {
var title = originalTitle === "" ? "Evennia" : originalTitle;
var options = {
body: text.replace(/(<([^>]+)>)/ig,""),
icon: "/static/website/images/evennia_logo.png"
var n = new Notification(title, options);
n.onclick = function(e) {
e.preventDefault();
window.focus();
this.close();
}
}
var n = new Notification(title, options);
n.onclick = function(e) {
e.preventDefault();
window.focus();
this.close();
}
}
});
}
if (("notification_sound" in options) && (options["notification_sound"])) {
var audio = new Audio("/static/webclient/media/notification.wav");
audio.play();
});
}
if (("notification_sound" in options) && (options["notification_sound"])) {
var audio = new Audio("/static/webclient/media/notification.wav");
audio.play();
}
}
}
}
@ -427,7 +428,9 @@ function doStartDragDialog(event) {
// Event when client finishes loading
$(document).ready(function() {
Notification.requestPermission();
if ("Notification" in window) {
Notification.requestPermission();
}
favico = new Favico({
animation: 'none'