mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 23:36:30 +01:00
commit
f10bb23ea0
68 changed files with 1336 additions and 1614 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
0
src/web/news/__init__.py → game/gamesrc/web/__init__.py
Executable file → Normal file
0
src/web/news/__init__.py → game/gamesrc/web/__init__.py
Executable file → Normal file
1
game/gamesrc/web/examples/__init__.py
Normal file
1
game/gamesrc/web/examples/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'kelketek'
|
||||
28
game/gamesrc/web/examples/urls.py
Normal file
28
game/gamesrc/web/examples/urls.py
Normal 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
|
||||
1
game/gamesrc/web/media/README.md
Normal file
1
game/gamesrc/web/media/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
This directory is where file uploads from Django apps are placed by default.
|
||||
3
game/gamesrc/web/static/README.md
Normal file
3
game/gamesrc/web/static/README.md
Normal 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.
|
||||
7
game/gamesrc/web/static_overrides/README.md
Normal file
7
game/gamesrc/web/static_overrides/README.md
Normal 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.
|
||||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
221
src/server/oob_cmds.py
Normal 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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
68
src/typeclasses/admin.py
Normal 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)
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -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'),
|
||||
)
|
||||
|
|
@ -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’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)
|
||||
|
Before Width: | Height: | Size: 678 KiB After Width: | Height: | Size: 678 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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 %}
|
||||
|
|
@ -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> </td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.perms.change %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
|
||||
{% else %}
|
||||
<td> </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> </td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.perms.change %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
|
||||
{% else %}
|
||||
<td> </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> </td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.perms.change %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
|
||||
{% else %}
|
||||
<td> </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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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> ›
|
||||
<a href="../../">{{ app_label|capfirst|escape }}</a> ›
|
||||
{% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} ›
|
||||
{% 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 %}
|
||||
|
|
@ -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>
|
||||
›
|
||||
<a href="../">
|
||||
{{ app_label|capfirst }}
|
||||
</a>
|
||||
›
|
||||
{{ 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 %}
|
||||
|
|
@ -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> <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>
|
||||
|
|
@ -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}}
|
||||
|
|
@ -45,13 +46,13 @@
|
|||
<div class="headerLinks">
|
||||
<span class="doNotDisplay">Tools:</span>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="/accounts/logout/">Log Out «</a>
|
||||
<a href="{% url 'logout' %}">Log Out «</a>
|
||||
<span class="doNotDisplay">|</span>
|
||||
Logged in as {{user.username}} «
|
||||
{% else %}
|
||||
<a href="/accounts/login/">Log In «</a>
|
||||
<a href="{% url 'login' %}">Log In «</a>
|
||||
<span class="doNotDisplay">|</span>
|
||||
<a href="/tbi">Register «</a>
|
||||
<a href="{% url 'to_be_implemented' %}">Register «</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>
|
||||
|
|
|
|||
22
src/web/templates/prosimii/evennia_admin.html
Normal file
22
src/web/templates/prosimii/evennia_admin.html
Normal 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 %}
|
||||
|
|
@ -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 »</a></div>
|
||||
|
||||
<p class="filler"><!-- Filler para to extend left vertical line--></p>
|
||||
</div-->
|
||||
</div>
|
||||
|
||||
<div class="rowOfBoxes dividingBorderAbove">
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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})
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
#
|
||||
# Define database entities for the app.
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
|
@ -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'),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue