Merge pull request #4 from evennia/master

Update from original.
This commit is contained in:
n0q 2014-06-29 14:10:09 -04:00
commit f10bb23ea0
68 changed files with 1336 additions and 1614 deletions

9
.gitignore vendored
View file

@ -26,6 +26,12 @@ __pycache__
*.restart
*.db3
# Installation-specific
game/settings.py
game/logs/*.log.*
game/gamesrc/web/static/*
game/gamesrc/web/media/*
# Installer logs
pip-log.txt
@ -41,3 +47,6 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
# PyCharm config
.idea

View file

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

View file

@ -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()

View file

View file

@ -0,0 +1 @@
__author__ = 'kelketek'

View file

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

View file

@ -0,0 +1 @@
This directory is where file uploads from Django apps are placed by default.

View file

@ -0,0 +1,3 @@
DO NOT EDIT FILES IN THIS DIRECTORY! THEY WILL BE OVERWRITTEN.
If you need to edit static files, see the static_overrides directory.

View file

@ -0,0 +1,7 @@
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
...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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <i>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 "
"<i>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':"<i>These are permissions/locks for in-game use. They are unrelated to website access rights.</i>"}),
'description': "<i>These are permissions/locks for in-game use. "
"They are unrelated to website access rights.</i>"}),
("In-game Player data",
{'fields':('db_typeclass_path', 'db_cmdset_storage'),
'description':"<i>These fields define in-game-specific properties for the Player object in-game.</i>"}),
)
{'fields': ('db_typeclass_path', 'db_cmdset_storage'),
'description': "<i>These fields define in-game-specific properties "
"for the Player object in-game.</i>"}))
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': "<i>These are not used in the default system.</i>"}),
('Website dates', {'fields': ('last_login', 'date_joined'),
'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights.</i>"}),
('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'),
'description': '<i>These are attributes that are more relevant to gameplay.</i>'}))
#('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'),
# 'description': '<i>These are attributes that are more relevant to gameplay.</i>'}))
('Website profile', {
'fields': ('first_name', 'last_name'),
'description': "<i>These are not used "
"in the default system.</i>"}),
('Website dates', {
'fields': ('last_login', 'date_joined'),
'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for "
"accessing the admin site. They are unrelated to "
"in-game access rights.</i>"}),
('Game Options', {
'fields': ('db_typeclass_path', 'db_cmdset_storage',
'db_lock_storage'),
'description': '<i>These are attributes that are more relevant '
'to gameplay.</i>'}))
# ('Game Options', {'fields': (
# 'db_typeclass_path', 'db_cmdset_storage',
# 'db_permissions', 'db_lock_storage'),
# 'description': '<i>These are attributes that are '
# 'more relevant to gameplay.</i>'}))
add_fieldsets = (
(None,
{'fields': ('username', 'password1', 'password2', 'email'),
'description':"<i>These account details are shared by the admin system and the game.</i>"},),)
'description': "<i>These account details are shared by the admin "
"system and the game.</i>"},),)
# 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,

View file

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

View file

@ -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):

View file

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

221
src/server/oob_cmds.py Normal file
View file

@ -0,0 +1,221 @@
"""
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.
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_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=("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.
"""
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)
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.
"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):
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.",)))
##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):
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, 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
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":
# 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":
# Not implemented (game specific)
pass
else:
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.
# 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 value points either to a database field
# (starting with db_*) or an Attribute name.
OOB_REPORTABLE = {
"CHARACTER_NAME": "db_key",
"ROOM_NAME": "db_location",
"TEST" : "test"
}

View file

@ -1,311 +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
class OOBFieldTracker(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 OOBAttributeTracker(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 read as being 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, OOBFieldTracker)
else: # assume attribute
oobhandler.track_attribute(obj, session.sessid,
key, OOBAttributeTracker)
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)

View file

@ -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
@ -45,7 +44,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,15 +54,23 @@ _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)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
session.msg(oob=("send", {"ERROR": errmsg}))
"Error wrapper"
session.msg(oob=("err", ("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
@ -97,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
@ -123,9 +130,12 @@ class TrackerHandler(object):
logger.log_trace()
# 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
@ -139,111 +149,87 @@ class TrackerBase(object):
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()
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)
kwargs[self.fieldname] = new_value
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", *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)
kwargs[self.attrname] = new_value
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", *args, **kwargs)
# Ticker of auto-updating objects
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
class OOBTickerHandler(TickerHandler):
ticker_pool_class = OOBTickerPool
# Main OOB Handler
class OOBHandler(object):
@ -258,8 +244,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 +256,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):
"""
@ -287,30 +266,20 @@ 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)
# make sure to purce the storage
#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()
#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 +291,16 @@ 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
#print "_track:", obj, id(obj), obj.__dict__
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
@ -339,14 +309,13 @@ class OOBHandler(object):
obj = obj.dbobj
except AttributeError:
pass
try:
# call at_delete hook
_GA(obj, "_trackerhandler").remove(fieldname, trackerclass, *args, **kwargs)
# call at_remove hook on the trackerclass
_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):
@ -354,25 +323,25 @@ 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):
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.
"""
# 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):
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
@ -380,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)
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.
"""
@ -395,48 +365,25 @@ 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.")
#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)
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)
#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)
self.tickerhandler.remove(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()
@ -445,23 +392,35 @@ 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):
"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()

View file

@ -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,10 +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)"
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,
@ -268,32 +290,9 @@ 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
plugin_module.start_plugin_services(PORTAL)

View file

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

View file

@ -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"
@ -30,13 +31,15 @@ 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
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):
"""
This is called when the connection is first established.
@ -72,7 +75,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.
@ -81,10 +84,9 @@ 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(oob=(key, args, kwargs))
for (key, args) in oobdata.items():
#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:
@ -118,6 +120,7 @@ class WebSocketProtocol(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)

View file

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

View file

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

View file

@ -103,8 +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)
allowed oob structures are
(The 'oob' kwarg of the msg() method).
Allowed input oob structures are:
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))
@ -134,23 +135,26 @@ 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, cmdname
return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {}))
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),
# (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 (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),)

View file

@ -58,10 +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 SSH protocol (SecureShell)
# 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_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]
@ -79,6 +91,9 @@ WEBSOCKET_ENABLED = False
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.
@ -98,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'
@ -224,7 +240,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
@ -441,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
@ -473,7 +484,7 @@ 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.
# 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 = '/'
@ -487,12 +498,23 @@ 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")
# 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.
@ -534,6 +556,7 @@ INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.admindocs',
'django.contrib.flatpages',
'django.contrib.staticfiles',
'src.server',
'src.typeclasses',
'src.players',
@ -541,8 +564,7 @@ INSTALLED_APPS = (
'src.comms',
'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"

68
src/typeclasses/admin.py Normal file
View file

@ -0,0 +1,68 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.core.urlresolvers import reverse
from src.typeclasses.models import Attribute, Tag
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 = 1
#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 '<strong><a href="%s">%s</a></strong>' % (
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')
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)

View file

@ -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,8 +71,7 @@ _DA = object.__delattr__
#
#------------------------------------------------------------
#class Attribute(SharedMemoryModel):
class Attribute(WeakSharedMemoryModel):
class Attribute(SharedMemoryModel):
"""
Abstract django model.
@ -99,20 +100,34 @@ class Attribute(WeakSharedMemoryModel):
# 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. 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")
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()
@ -173,13 +188,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):
@ -233,10 +241,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):
@ -558,12 +570,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."

View file

@ -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),

View file

@ -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('<textarea{0}>\r\n{1}</textarea>',
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, SyntaxError):
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)

View file

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

View file

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

View file

@ -1,13 +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 = patterns('src.web.news.views',
(r'^show/(?P<entry_id>\d+)/$', 'show_news'),
(r'^archive/$', 'news_archive'),
(r'^search/$', 'search_form'),
(r'^search/results/$', 'search_results'),
)

View file

@ -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 = """
<p class='doNotDisplay doNotPrint'>This page&rsquo;s menu:</p>
<ul id='side-bar'>
<li><a href='/news/archive'>News Archive</a></li>
<li><a href='/news/search'>Search News</a></li>
</ul>
"""
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)

View file

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 678 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

@ -1,11 +0,0 @@
{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }} | {% trans 'Evennia site admin' %}{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'Evennia database administration' %}
<a href="/">(Back)</a> </h1>
{% endblock %}
{% block nav-global %}{% endblock %}

View file

@ -1,184 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}dashboard{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
<div id="content-main">
{% if app_list %}
{% for app in app_list %}
{% if app.name in evennia_userapps %}
{% if app.name == 'Players' %}
<h2>Admin</h2>
<p><i>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).</i></p>
{% endif %}
<div class="module">
<table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
<caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
{% for model in app.models %}
<tr>
{% if model.name == "Players" %}
{% if model.perms.change %}
<th scope="row"><a href="{{ model.admin_url }}">Player</a></th>
{% else %}
<th scope="row">Player</th>
{% endif %}
{% if model.perms.add %}
<td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
{% if model.perms.change %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
</tr>
{% endif %}
{% endfor %}
</table>
</div>
{% endif %}
{% endfor %}
<h2>Game entities</h2>
{% for app in app_list %}
{% if app.name in evennia_entityapps %}
{% if app.name == 'Comms' %}
<p><i>This defines entities that has an in-game precense or
effect of some kind.</i></p>
{% endif %}
<div class="module">
<table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
<caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
{% for model in app.models %}
<tr>
{% if model.perms.change %}
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.perms.add %}
<td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
{% if model.perms.change %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endfor %}
<h2>Website</h2>
{% for app in app_list %}
{% if app.name in evennia_websiteapps %}
{% if app.name == 'Flatpages' %}
<p><i>Miscellaneous objects related to the running and
managing of the Web presence.</i></p>
{% endif %}
<div class="module">
<table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
<caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
{% for model in app.models %}
<tr>
{% if model.perms.change %}
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.perms.add %}
<td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
{% if model.perms.change %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endfor %}
{% else %}
<p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>{% trans 'Recent Actions' %}</h2>
<h3>{% trans 'My Actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None yet.' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br/>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% trans 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -1,14 +0,0 @@
{% extends "admin/players/change_form.html" %}
{% load i18n %}
{% block form_top %}
{% if not is_popup %}
<p>{% trans "First, enter a username and password. Then you'll be able to edit more Player options." %}</p>
{% else %}
<p>{% trans "Enter a username and password." %}</p>
{% endif %}
{% endblock %}
{% block after_field_sets %}
<script type="text/javascript">document.getElementById("id_username").focus();</script>
{% endblock %}

View file

@ -1,70 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_modify admin_static %}
{% block extrahead %}{{ block.super }}
{% url 'admin:jsi18n' as jsi18nurl %}
<script type="text/javascript" src="{{ jsi18nurl|default:"../../../jsi18n/" }}"></script>
{{ media }}
{% endblock %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% 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 %}
<div class="breadcrumbs">
<a href="../../../">{% trans "Home" %}</a> &rsaquo;
<a href="../../">{{ app_label|capfirst|escape }}</a> &rsaquo;
{% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} &rsaquo;
{% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endif %}{% endblock %}
{% block content %}<div id="content-main">
{% block object-tools %}
{% if change %}{% if not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
<li><a href="history/" class="historylink">{% trans "History" %}</a></li>
{% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %}
</ul>
{% endif %}{% endif %}
{% endblock %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if save_on_top %}{% submit_row %}{% endif %}
{% if errors %}
<p class="errornote">
{% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
</p>
{{ 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 %}
<script type="text/javascript">document.getElementById("{{ adminform.first_field.id_for_label }}").focus();</script>
{% endif %}
{# JavaScript for prepopulated fields #}
{% prepopulated_fields_js %}
</div>
</form></div>
{% endblock %}

View file

@ -1,103 +0,0 @@
{% extends "admin/base_site.html" %}
{% load admin_static admin_list i18n %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
{% if cl.formset %}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
{% endif %}
{% if cl.formset or action_form %}
{% url 'admin:jsi18n' as jsi18nurl %}
<script type="text/javascript" src="{{ jsi18nurl|default:'../../jsi18n/' }}"></script>
{% endif %}
{{ media.css }}
{% if not actions_on_top and not actions_on_bottom %}
<style>
#changelist table thead th:first-child {width: inherit}
</style>
{% endif %}
{% endblock %}
{% block extrahead %}
{{ block.super }}
{{ media.js }}
{% if action_form %}{% if actions_on_top or actions_on_bottom %}
<script type="text/javascript">
(function($) {
$(document).ready(function($) {
$("tr input.action-select").actions();
});
})(django.jQuery);
</script>
{% endif %}{% endif %}
{% endblock %}
{% block bodyclass %}change-list{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="../../">
{% trans "Home" %}
</a>
&rsaquo;
<a href="../">
{{ app_label|capfirst }}
</a>
&rsaquo;
{{ cl.opts.verbose_name_plural|capfirst }}
</div>
{% endblock %}
{% endif %}
{% block coltype %}flex{% endblock %}
{% block content %}
<div id="content-main">
{% block object-tools %}
{% if has_add_permission %}
<ul class="object-tools">
{% block object-tools-items %}
<li>
<a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
{% endblock %}
</ul>
{% endif %}
{% endblock %}
{% if cl.formset.errors %}
<p class="errornote">
{% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
</p>
{{ cl.formset.non_form_errors }}
{% endif %}
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %}
{% block filters %}
{% if cl.has_filters %}
<div id="changelist-filter">
<h2>{% trans 'Filter' %}</h2>
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
</div>
{% endif %}
{% endblock %}
<form id="changelist-form" action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>{% csrf_token %}
{% if cl.formset %}
<div>{{ cl.formset.management_form }}</div>
{% 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 %}
</form>
</div>
</div>
{% endblock %}

View file

@ -1,82 +0,0 @@
{% load i18n admin_static %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<!--h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2-->
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<!--h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3-->
{% 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 }}
</div>{% endfor %}
</div>
<script type="text/javascript">
(function($) {
$(document).ready(function() {
var rows = "#{{ inline_admin_formset.formset.prefix }}-group .inline-related";
var updateInlineLabel = function(row) {
$(rows).find(".inline_label").each(function(i) {
var count = i + 1;
$(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
});
}
var reinitDateTimeShortCuts = function() {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts != "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
}
var updateSelectFilter = function() {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter != "undefined"){
$(".selectfilter").each(function(index, value){
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% static "admin/" %}");
});
$(".selectfilterstacked").each(function(index, value){
var namearr = value.name.split('-');
SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% static "admin/" %}");
});
}
}
var initPrepopulatedFields = function(row) {
row.find('.prepopulated_field').each(function() {
var field = $(this);
var input = field.find('input, select, textarea');
var dependency_list = input.data('dependency_list') || [];
var dependencies = [];
$.each(dependency_list, function(i, field_name) {
dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id'));
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr('maxlength'));
}
});
}
$(rows).formset({
prefix: "{{ inline_admin_formset.formset.prefix }}",
addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
deleteCssClass: "inline-deletelink",
deleteText: "{% trans "Remove" %}",
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: (function(row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
})
});
});
})(django.jQuery);
</script>

View file

@ -1,3 +1,4 @@
{% load staticfiles %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@ -8,12 +9,12 @@
<meta name="generator" content="haran" />
{% if sidebar %}
<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}css/prosimii-screen-alt.css" media="screen" title="Prosimii (Sidebar)" />
<link rel="stylesheet" type="text/css" href="{% static "evennia_general/css/prosimii-screen-alt.css" %}" media="screen" title="Prosimii (Sidebar)" />
{% else %}
<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}css/prosimii-screen.css" media="screen" title="Prosimii" />
<link rel="stylesheet" type="text/css" href="{% static "evennia_general/css/prosimii-screen.css" %}" media="screen" title="Prosimii" />
{% endif %}
<link rel="stylesheet alternative" type="text/css" href="{{MEDIA_URL}}css/prosimii-print.css" media="screen" title="Print Preview" />
<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}css/prosimii-print.css" media="print" />
<link rel="stylesheet alternative" type="text/css" href="{% static "evennia_general/css/prosimii-print.css" %}" media="screen" title="Print Preview" />
<link rel="stylesheet" type="text/css" href="{% static "evennia_general/css/prosimii-print.css" %}" media="print" />
{% block header_ext %}
{% endblock %}
@ -34,7 +35,7 @@
</div>
<div class="midHeader">
<img src="/media/images/evennia_logo_small.png" align='left'/> <h1 class="headerTitle" lang="la">{{game_name}}</h1>
<img src="{% static "evennia_general/images/evennia_logo_small.png" %}" align='left'/> <h1 class="headerTitle" lang="la">{{game_name}}</h1>
<div class="headerSubTitle" title="Slogan">
<!-- Insert a slogan here if you want -->
{{game_slogan}} &nbsp;
@ -45,13 +46,13 @@
<div class="headerLinks">
<span class="doNotDisplay">Tools:</span>
{% if user.is_authenticated %}
<a href="/accounts/logout/">Log Out &laquo;</a>
<a href="{% url 'logout' %}">Log Out &laquo;</a>
<span class="doNotDisplay">|</span>
Logged in as {{user.username}} &laquo;
{% else %}
<a href="/accounts/login/">Log In &laquo;</a>
<a href="{% url 'login' %}">Log In &laquo;</a>
<span class="doNotDisplay">|</span>
<a href="/tbi">Register &laquo;</a>
<a href="{% url 'to_be_implemented' %}">Register &laquo;</a>
{% endif %}
</div>
</div>
@ -61,9 +62,9 @@
<a href="/">Home</a> |
<a href="https://github.com/evennia/evennia/wiki/Evennia-Introduction/">About</a> |
<a href="https://github.com/evennia/evennia/wiki">Documentation</a> |
<a href="/admin/">Admin Interface</a>
<a href="{% url 'admin:index' %}">Admin Interface</a>
{% if webclient_enabled %}
| <a href="/webclient">Play Online</a>
| <a href="{% url 'webclient:index' %}">Play Online</a>
{% endif %}
</div>
@ -87,7 +88,7 @@
title="Other designs by haran">haran</a>.
Powered by
<a href="http://evennia.com">Evennia.</a>
<br \>
<br />
</span>
</div>
</body>

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
<h1>Admin</h1>
Welcome to the Evennia Admin Page. Here, you can edit many facets of players, characters, and other parts of the game.
<h2><a href="{% url "admin:players_playerdb_changelist" %}">Players</a></h2>
Players are user accounts. Players can have several characters under them. A user's login and password information can be changed here.
<h2><a href="{% url "admin:objects_objectdb_changelist" %}">Objects</a></h2>
Objects include everything from characters to rooms to exits.
<h2><a href="{% url "admin:scripts_scriptdb_changelist" %}">Scripts</a></h2>
Scripts are meta objects used to store game information, handle special functionality or perform timed actions.
<h2><a href="{% url "admin:comms_channeldb_changelist" %}">Channels</a></h2>
Channels are used for player communications.
<h2><a href="{% url "admin:help_helpentry_changelist" %}">Help Topics</a></h2>
<p>If you are an advanced user who needs access to the raw Django Admin, it is available <a href="{% url "django_admin" %}">here</a>.
You can make this the default my changing <code>EVENNIA_ADMIN</code> to <code>False</code> in <code>settings.py</code> and reload.</p>
{% endblock content %}

View file

@ -14,11 +14,11 @@
<p>Welcome to your new installation of <a href="http://evennia.com">Evennia</a>, 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 <a href="/admin">admin interface</a> you can view and edit the
needed. Through the <a href="{% url 'admin:index' %}">admin interface</a> 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
<a href='/webclient'>online client</a>!<br></br>
<a href="{% url 'webclient:index' %}">online client</a>!<br></br>
{% endif %}
For more info, take your time to
peruse our extensive online <a href="https://github.com/evennia/evennia/wiki">documentation</a>.
@ -28,21 +28,6 @@
<a href="https://groups.google.com/forum/#!forum/evennia">mailing list</a> or to come say hi in the <a href="http://webchat.freenode.net/?channels=evennia">developer chatroom</a>. If you find bugs, please report them to our <a href="https://github.com/evennia/evennia/issues">Issue tracker</a>.
</p>
</div>
<!-- news app commented out since there are no news in the database originally -->
<!--div class="oneThird">
<h1>Latest News</h1>
{% for entry in news_entries %}
<a href="/news/show/{{entry.id}}" class="newsHeading">{{entry.topic.name}}: {{entry.title}}</a>
<p class="newsDate">By {{entry.author.username}} on {{entry.date_posted|time}}</p>
<p class="newsSummary">{{entry.body|truncatewords:20}}</p>
{% endfor %}
<div class="more"><a href="/news/archive">More News &raquo;</a></div>
<p class="filler"><!-- Filler para to extend left vertical line--></p>
</div-->
</div>
<div class="rowOfBoxes dividingBorderAbove">

View file

@ -1,51 +0,0 @@
{% extends "base.html" %}
{% block header_ext %}
{% endblock %}
{% block sidebar %}
{{sidebar|safe}}
{% endblock %}
{% block content %}
<h1 id="alt-layout">{{page_title}}</h1>
<strong>Navigation:</strong> <a href="{{browse_url}}/?page=1&search_terms={{searchtext|urlencode}}">First</a> |
{% if has_previous %}
<a href="{{browse_url}}/?page={{previous}}&search_terms={{searchtext|urlencode}}">Prev</a>
{% else %}
Prev
{% endif %}
| <em>{{page}}</em> of <em>{{pages}}</em> pages |
{% if has_next %}
<a href="{{browse_url}}/?page={{next}}&search_terms={{searchtext|urlencode}}">Next</a>
{% else %}
Next
{% endif %}
| <a href="{{browse_url}}/?page={{pages}}&search_terms={{searchtext|urlencode}}">Last</a>
{% for entry in object_list %}
<a href="/news/show/{{entry.id}}" class="newsHeading">{{entry.topic.name}}: {{entry.title}}</a>
<p class="newsDate">By {{entry.author.username}} on {{entry.date_posted|time}}</p>
<p class="newsSummary">{{entry.body|truncatewords:80}}</p>
{% endfor %}
<strong>Navigation:</strong> <a href="{{browse_url}}/?page=1&search_terms={{searchtext|urlencode}}">First</a> |
{% if has_previous %}
<a href="{{browse_url}}/?page={{previous}}&search_terms={{searchtext|urlencode}}">Prev</a>
{% else %}
Prev
{% endif %}
| <em>{{page}}</em> of <em>{{pages}}</em> pages |
{% if has_next %}
<a href="{{browse_url}}/?page={{next}}&search_terms={{searchtext|urlencode}}">Next</a>
{% else %}
Next
{% endif %}
| <a href="{{browse_url}}/?page={{pages}}&search_terms={{searchtext|urlencode}}">Last</a>
{% endblock %}

View file

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% block header_ext %}
{% endblock %}
{% block sidebar %}
{{sidebar|safe}}
{% endblock %}
{% block content %}
<h1 id="alt-layout">Search News</h1>
<p>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.</p>
<form method="GET">
{{search_form.search_terms}}
<button type="Submit">Search</button>
</form>
{% endblock %}

View file

@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block header_ext %}
{% endblock %}
{% block sidebar %}
{{sidebar|safe}}
{% endblock %}
{% block content %}
<h1 id="alt-layout">{{news_entry.topic.name}}: {{news_entry.title}}</h1>
<p class="newsDate">By {{news_entry.author.username}} on {{news_entry.date_posted|time}}</p>
<p class="newsSummary">{{news_entry.body}}</p>
{% endblock %}

View file

@ -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
@ -24,33 +24,45 @@ 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'),
# Front page
url(r'^', include('src.web.website.urls')),
# News stuff
# url(r'^news/', include('src.web.news.urls')),
url(r'^accounts/login', 'django.contrib.auth.views.login', name="login"),
url(r'^accounts/logout', 'django.contrib.auth.views.logout', name="logout"),
# 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')),
url(r'^admin/', include(admin.site.urls)),
# favicon
url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')),
# ajax stuff
url(r'^webclient/',include('src.web.webclient.urls')),
)
url(r'^webclient/', include('src.web.webclient.urls', namespace='webclient', app_name='webclient')),
# Front page
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).
if settings.SERVE_MEDIA:
urlpatterns += patterns('',
(r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
)
urlpatterns.extend([
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
url(r'^static/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT})
])

View file

@ -1,12 +1,11 @@
#
# 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
from django.conf import settings
from src.utils.utils import get_evennia_version
@ -19,8 +18,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']
@ -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,5 +46,7 @@ def general_context(request):
'evennia_setupapps': GAME_SETUP,
'evennia_connectapps': CONNECTIONS,
'evennia_websiteapps':WEBSITE,
"webclient_enabled" : settings.WEBCLIENT_ENABLED
"webclient_enabled" : WEBCLIENT_ENABLED,
"websocket_enabled" : WEBSOCKET_CLIENT_ENABLED,
"websocket_url" : WSURL
}

View file

@ -5,18 +5,17 @@ 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.
@ -26,14 +25,12 @@ 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"
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 "no one"
nobjs = ObjectDB.objects.all().count()
nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()
@ -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",
@ -56,8 +52,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 +65,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)

View file

@ -0,0 +1,348 @@
/*
Evennia websocket webclient (javascript component)
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
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.
*/
// 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
//
// Custom OOB functions
// functions defined here can be called by name by the server. For
// 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) {
// example echo function.
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
//
function webclient_init(){
// called when client is just initializing
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) {
// called when client is first connecting
$("#connecting").remove(); // remove the "connecting ..." message
doShow("sys", "Using websockets - connected to " + wsurl + ".")
setTimeout(function () {
$("#playercount").fadeOut('slow', doSetSizes);
}, 10000);
}
function onClose(evt) {
// called when client is closing
CLIENT_HASH = 0;
alert("Mud client connection was closed cleanly.");
}
function onMessage(evt) {
// 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
doShow('out', inmsg);
return;
}
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 {
// normal message
doShow('out', inmsg); }
}
function onError(evt) {
// called on a server error
doShow('err', "Error: Server returned an error. Try reloading the page.");
}
function doSend(){
// 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 (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));
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); }
}
function doOOB(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);
}
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(
"<div class='msg "+ type +"'>"+ msg +"</div>");
// scroll message window to bottom
$('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight});
}
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();
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();
}
});
};
// Input jQuery callbacks
$(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
//doShow("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)
// Callback function - called when the browser window resizes
$(window).resize(doSetSizes);
// 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
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() {
websocket.send("idle");
}, 60000*3);
});

View file

@ -1,4 +1,5 @@
<!DOCTYPE html>
{% load staticfiles %}
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
@ -6,16 +7,29 @@
<title>Evennia web MUD client</title>
<!--CSS style sheet -->
<link rel='stylesheet' type="text/css" media="screen" href="/media/css/webclient.css">
<link rel='stylesheet' type="text/css" media="screen" href="{% static "webclient/css/webclient.css" %}">
<!-- Importing the online jQuery javascript library -->
<script src="http://code.jquery.com/jquery-1.6.1.js" type="text/javascript" charset="utf-8"></script>
<!--script src="http://code.jquery.com/jquery-1.6.1.js" type="text/javascript" charset="utf-8"></script-->
<script src="http://code.jquery.com/jquery-1.11.1.min.js" type="text/javascript" charset="utf-8"></script>
<!--for offline testing, download the jquery library from jquery.com-->
<!--script src="/media/javascript/jquery-1.4.4.js" type="text/javascript" charset="utf-8"></script-->
<!--script src="/media/javascript/jquery-1.11.1.js" type="text/javascript" charset="utf-8"></script-->
<!-- Importing the Evennia ajax webclient component (requires jQuery) -->
<script src="/media/javascript/evennia_webclient.js" type="text/javascript" charset="utf-8"></script>
{% if websocket_enabled %}
<script language="javascript" type="text/javascript">
if ("WebSocket" in window) {
<!-- Importing the Evennia websocket webclient component (requires jQuery) -->
var wsurl = "{{websocket_url}}";
document.write("\<script src=\"{% static "webclient/js/evennia_websocket_webclient.js" %}\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")}
else {
<!-- No websocket support in browser. Importing the Evennia ajax webclient component (requires jQuery) -->
document.write("\<script src=\"{% static "webclient/js/evennia_ajax_webclient.js" %}\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")}
</script>
{% else %}
<!-- websocket not enabled; use ajax -->
<script src="{% static "webclient/js/evennia_ajax_webclient.js" %}" type="text/javascript" charset="utf-8"></script>
{% endif %}
</head>
<body>
@ -38,7 +52,7 @@
</div>
<form id="inputform" action="javascript:void(0);">
<div id="playercount">Logged in Players: {{num_players_connected}}</div>
<div id="inputcontrol"><input type="text" id="inputfield" autocomplete="off"><input id="inputsend" type="button" value="send" onClick="webclient_input()" /></div>
<div id="inputcontrol"><input type="text" id="inputfield" autocomplete="off"><input id="inputsend" type="button" value="send" onClick="doSend()"/></div>
</form>
</div>

View file

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

View file

@ -4,11 +4,10 @@ This contains a simple view for rendering the webclient
page and serve it eventual static content.
"""
from django.shortcuts import render
from src.players.models import PlayerDB
from django.shortcuts import render_to_response, redirect
from django.template import RequestContext
from django.conf import settings
from src.server.sessionhandler import SESSIONS
def webclient(request):
"""
@ -21,8 +20,8 @@ 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)
return render(request, 'webclient.html', pagevars)

View file

@ -1,7 +0,0 @@
#
# Define database entities for the app.
#
from django.db import models

View file

@ -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'),
)