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 Note that the only way to retrieve
an object from a None location is by direct #dbref an object from a None location is by direct #dbref
reference. A puppeted object cannot be moved to None. 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 Teleports an object somewhere. If no object is given, you yourself
is teleported to the target location. """ is teleported to the target location. """
@ -2343,6 +2344,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
# setting switches # setting switches
tel_quietly = "quiet" in switches tel_quietly = "quiet" in switches
to_none = "tonone" in switches to_none = "tonone" in switches
to_loc = "loc" in switches
if to_none: if to_none:
# teleporting to None # teleporting to None
@ -2368,7 +2370,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
# not teleporting to None location # not teleporting to None location
if not args and not to_none: 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 return
if rhs: if rhs:
@ -2384,6 +2386,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
if not destination: if not destination:
caller.msg("Destination not found.") caller.msg("Destination not found.")
return return
if to_loc:
destination = destination.location
if not destination:
caller.msg("Destination has no location.")
return
if obj_to_teleport == destination: if obj_to_teleport == destination:
caller.msg("You can't teleport an object inside of itself!") caller.msg("You can't teleport an object inside of itself!")
return 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) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
HELP_MORE = settings.HELP_MORE HELP_MORE = settings.HELP_MORE
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
# limit symbol import for API # limit symbol import for API
__all__ = ("CmdHelp", "CmdSetHelp") __all__ = ("CmdHelp", "CmdSetHelp")
@ -231,6 +232,15 @@ class CmdHelp(Command):
# try an exact command auto-help match # try an exact command auto-help match
match = [cmd for cmd in all_cmds if cmd == query] 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: if len(match) == 1:
formatted = self.format_help_entry(match[0].key, formatted = self.format_help_entry(match[0].key,
match[0].get_help(caller, cmdset), match[0].get_help(caller, cmdset),

View file

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

View file

@ -93,7 +93,6 @@ class WebSocketClient(Protocol, Session):
csession = self.get_client_session() csession = self.get_client_session()
if csession: if csession:
print("In disconnect: csession uid=%s" % csession.get("webclient_authenticated_uid", None))
csession["webclient_authenticated_uid"] = None csession["webclient_authenticated_uid"] = None
csession.save() csession.save()
self.logged_in = False 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. # using POST requests to /webclientdata.
# #
class WebClient(resource.Resource): class AjaxWebClient(resource.Resource):
""" """
An ajax/comet long-polling transport An ajax/comet long-polling transport
@ -163,13 +163,13 @@ class WebClient(resource.Resource):
remote_addr = request.getClientIP() remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port) host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port)
sess = WebClientSession() sess = AjaxWebClientSession()
sess.client = self sess.client = self
sess.init_session("ajax/comet", remote_addr, self.sessionhandler) sess.init_session("ajax/comet", remote_addr, self.sessionhandler)
sess.csessid = csessid sess.csessid = csessid
csession = _CLIENT_SESSIONS(session_key=sess.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: if uid:
# the client session is already logged in # the client session is already logged in
sess.uid = uid sess.uid = uid
@ -292,14 +292,26 @@ class WebClient(resource.Resource):
# web client interface. # 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): def __init__(self, *args, **kwargs):
self.protocol_name = "ajax/comet" 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."): def disconnect(self, reason="Server disconnected."):
""" """
@ -308,10 +320,22 @@ class WebClientSession(session.Session):
Args: Args:
reason (str): Motivation for the disconnect. 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.lineSend(self.csessid, ["connection_close", [reason], {}])
self.client.client_disconnect(self.csessid) self.client.client_disconnect(self.csessid)
self.sessionhandler.disconnect(self) 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): def data_out(self, **kwargs):
""" """
Data Evennia -> User Data Evennia -> User

View file

@ -18,17 +18,28 @@ class TagAdmin(admin.ModelAdmin):
class TagForm(forms.ModelForm): class TagForm(forms.ModelForm):
""" """
This form overrides the base behavior of the ModelForm that would be used for a Tag-through-model. This form overrides the base behavior of the ModelForm that would be used for a
Since the through-models only have access to the foreignkeys of the Tag and the Object that they're Tag-through-model. Since the through-models only have access to the foreignkeys of the Tag and
attached to, we need to spoof the behavior of it being a form that would correspond to its tag, the Object that they're attached to, we need to spoof the behavior of it being a form that would
or the creation of a tag. Instead of being saved, we'll call to the Object's handler, which will handle correspond to its tag, or the creation of a tag. Instead of being saved, we'll call to the
the creation, change, or deletion of a tag for us, as well as updating the handler's cache so that all Object's handler, which will handle the creation, change, or deletion of a tag for us, as well
changes are instantly updated in-game. as updating the handler's cache so that all changes are instantly updated in-game.
""" """
tag_key = forms.CharField(label='Tag Name') tag_key = forms.CharField(label='Tag Name',
tag_category = forms.CharField(label="Category", required=False) required=True,
tag_type = forms.CharField(label="Type", required=False) help_text="This is the main key identifier")
tag_data = forms.CharField(label="Data", required=False) 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): def __init__(self, *args, **kwargs):
""" """
@ -121,8 +132,8 @@ class TagInline(admin.TabularInline):
form = TagForm form = TagForm
formset = TagFormSet formset = TagFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
raw_id_fields = ('tag',) # raw_id_fields = ('tag',)
readonly_fields = ('tag',) # readonly_fields = ('tag',)
extra = 0 extra = 0
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
@ -150,13 +161,26 @@ class AttributeForm(forms.ModelForm):
changes are instantly updated in-game. changes are instantly updated in-game.
""" """
attr_key = forms.CharField(label='Attribute Name', required=False, initial="Enter Attribute Name Here") 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_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", attr_strvalue = forms.CharField(label="String Value",
help_text="Only enter this if value is blank and you want to save as a string", help_text="Only set when using the Attribute as a string-only store",
required=False) required=False,
attr_lockstring = forms.CharField(label="Locks", required=False, widget=forms.Textarea) 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): 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, 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 and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
similarly be populated. similarly be populated.
""" """
super(AttributeForm, self).__init__(*args, **kwargs) super(AttributeForm, self).__init__(*args, **kwargs)
attr_key = None attr_key = None
@ -261,8 +286,8 @@ class AttributeInline(admin.TabularInline):
form = AttributeForm form = AttributeForm
formset = AttributeFormSet formset = AttributeFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
raw_id_fields = ('attribute',) # raw_id_fields = ('attribute',)
readonly_fields = ('attribute',) # readonly_fields = ('attribute',)
extra = 0 extra = 0
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):

View file

@ -126,7 +126,13 @@ class PickledWidget(Textarea):
except ValueError: except ValueError:
return value 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>', return format_html('<textarea{0}>\r\n{1}</textarea>',
flatatt(final_attrs), flatatt(final_attrs),
value) 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 long-polling (AJAX/COMET) type of connection (using
evennia/server/portal/webclient_ajax.py) 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], ["cmdname", args, kwargs],
where args is an JSON array and kwargs is a JSON object that will be where args is an JSON array and kwargs is a JSON object. These will be both
used as argument to call the cmdname function. used as arguments emitted to a callback named "cmdname" as cmdname(args, kwargs).
This library makes the "Evennia" object available. It has the This library makes the "Evennia" object available. It has the
following official functions: following official functions:
@ -45,7 +45,7 @@ An "emitter" object must have a function
relay the data to its correct gui element. relay the data to its correct gui element.
- The default emitter also has the following methods: - The default emitter also has the following methods:
- on(cmdname, listener) - this ties a listener to the backend. This function - 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. - off(cmdname) - remove the listener for this cmdname.
*/ */

View file

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