diff --git a/.gitignore b/.gitignore index 878233d84f..93408a842a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 2153c276f3..260e57acac 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Evennia MUD/MU\* Creation System ================================ -*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom. +*Evennia* is a Python-based MUD/MU\* server/codebase using modern technologies. It is made available as open source under the very friendly [BSD license](https://github.com/evennia/evennia/wiki/Licensing). Evennia allows creators to design and flesh out text-based massively-multiplayer online games with great freedom. http://www.evennia.com is the main hub tracking all things Evennia. The documentation wiki is found [here](https://github.com/evennia/evennia/wiki). diff --git a/game/evennia.py b/game/evennia.py index d1ed65fd69..998b078cb5 100755 --- a/game/evennia.py +++ b/game/evennia.py @@ -16,6 +16,8 @@ from optparse import OptionParser from subprocess import Popen # Set the Python path up so we can get to settings.py from here. +from django.core import management + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' @@ -264,6 +266,8 @@ def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART return os.remove(pidfile) # set restart/norestart flag + if restart == 'reload': + management.call_command('collectstatic', interactive=False, verbosity=0) f = open(restart_file, 'w') f.write(str(restart)) f.close() @@ -389,11 +393,13 @@ def handle_args(options, mode, service): if inter: cmdstr.append('--iportal') cmdstr.append('--noserver') + management.call_command('collectstatic', verbosity=1, interactive=False) else: # all # for convenience we don't start logging of # portal, only of server with this command. if inter: cmdstr.extend(['--iserver']) + management.call_command('collectstatic', verbosity=1, interactive=False) return cmdstr elif mode == 'reload': @@ -425,6 +431,7 @@ def handle_args(options, mode, service): kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown") return None + def error_check_python_modules(): """ Import settings modules in settings. This will raise exceptions on @@ -509,7 +516,7 @@ def main(): if mode not in ['menu', 'start', 'reload', 'stop']: print "mode should be none, 'menu', 'start', 'reload' or 'stop'." sys.exit() - if service not in ['server', 'portal', 'all']: + if service not in ['server', 'portal', 'all']: print "service should be none, 'server', 'portal' or 'all'." sys.exit() diff --git a/src/web/news/__init__.py b/game/gamesrc/web/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from src/web/news/__init__.py rename to game/gamesrc/web/__init__.py diff --git a/game/gamesrc/web/examples/__init__.py b/game/gamesrc/web/examples/__init__.py new file mode 100644 index 0000000000..cdcf9604a4 --- /dev/null +++ b/game/gamesrc/web/examples/__init__.py @@ -0,0 +1 @@ +__author__ = 'kelketek' diff --git a/game/gamesrc/web/examples/urls.py b/game/gamesrc/web/examples/urls.py new file mode 100644 index 0000000000..58754f51c9 --- /dev/null +++ b/game/gamesrc/web/examples/urls.py @@ -0,0 +1,28 @@ +from django.conf.urls import url, include + +from src.web.urls import urlpatterns + +# +# File that determines what each URL points to. This uses _Python_ regular +# expressions, not Perl's. +# +# See: +# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3 +# + +# Copy this file into your web directory in gamesrc, and add this line to settings.py: +# ROOT_URLCONF = 'game.gamesrc.web.urls' + +# Add your own URL patterns to the patterns variable below, and then change +# +# These are Django URL patterns, so you should look up how to use these at +# https://docs.djangoproject.com/en/1.6/topics/http/urls/ + +# Follow the full Django tutorial to learn how to create web views for Evennia. +# https://docs.djangoproject.com/en/1.6/intro/tutorial01/ + +patterns = [ + # url(r'/desired/url/', view, name='example'), +] + +urlpatterns = patterns + urlpatterns \ No newline at end of file diff --git a/game/gamesrc/web/media/README.md b/game/gamesrc/web/media/README.md new file mode 100644 index 0000000000..5f2c3da7a8 --- /dev/null +++ b/game/gamesrc/web/media/README.md @@ -0,0 +1 @@ +This directory is where file uploads from Django apps are placed by default. \ No newline at end of file diff --git a/game/gamesrc/web/static/README.md b/game/gamesrc/web/static/README.md new file mode 100644 index 0000000000..10e1c27214 --- /dev/null +++ b/game/gamesrc/web/static/README.md @@ -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. diff --git a/game/gamesrc/web/static_overrides/README.md b/game/gamesrc/web/static_overrides/README.md new file mode 100644 index 0000000000..0bc428b141 --- /dev/null +++ b/game/gamesrc/web/static_overrides/README.md @@ -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. diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index 661cd91cdd..f77b7ad272 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -712,7 +712,7 @@ class CmdPage(MuxPlayerCommand): if isinstance(receiver, basestring): pobj = caller.search(receiver) elif hasattr(receiver, 'character'): - pobj = receiver.character + pobj = receiver else: self.msg("Who do you want to page?") return @@ -741,13 +741,13 @@ class CmdPage(MuxPlayerCommand): rstrings.append("You are not allowed to page %s." % pobj) continue pobj.msg("%s %s" % (header, message)) - if hasattr(pobj, 'has_player') and not pobj.has_player: + if hasattr(pobj, 'sessions') and not pobj.sessions: received.append("{C%s{n" % pobj.name) rstrings.append("%s is offline. They will see your message if they list their pages later." % received[-1]) else: received.append("{c%s{n" % pobj.name) if rstrings: - self.msg(rstrings="\n".join(rstrings)) + self.msg("\n".join(rstrings)) self.msg("You paged %s with: '%s'." % (", ".join(received), message)) diff --git a/src/comms/admin.py b/src/comms/admin.py index 2fd5da1fa3..5a8c790c99 100644 --- a/src/comms/admin.py +++ b/src/comms/admin.py @@ -5,6 +5,15 @@ from django.contrib import admin from src.comms.models import ChannelDB +from src.typeclasses.admin import AttributeInline, TagInline + + +class ChannelAttributeInline(AttributeInline): + model = ChannelDB.db_attributes.through + + +class ChannelTagInline(TagInline): + model = ChannelDB.db_tags.through class MsgAdmin(admin.ModelAdmin): @@ -21,6 +30,7 @@ class MsgAdmin(admin.ModelAdmin): class ChannelAdmin(admin.ModelAdmin): + inlines = [ChannelTagInline, ChannelAttributeInline] list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions") list_display_links = ("id", 'db_key') ordering = ["db_key"] diff --git a/src/objects/admin.py b/src/objects/admin.py index 3d4adeb6a1..78d7838c4f 100644 --- a/src/objects/admin.py +++ b/src/objects/admin.py @@ -6,27 +6,16 @@ from django import forms from django.conf import settings from django.contrib import admin -from src.typeclasses.models import Attribute, Tag +from src.typeclasses.admin import AttributeInline, TagInline from src.objects.models import ObjectDB -class AttributeInline(admin.TabularInline): - # This class is currently not used, because PickleField objects are - # not editable. It's here for us to ponder making a way that allows - # them to be edited. - model = Attribute - fields = ('db_key', 'db_value') - extra = 0 +class ObjectAttributeInline(AttributeInline): + model = ObjectDB.db_attributes.through -class TagInline(admin.TabularInline): +class ObjectTagInline(TagInline): model = ObjectDB.db_tags.through - raw_id_fields = ('tag',) - extra = 0 - - -class TagAdmin(admin.ModelAdmin): - fields = ('db_key', 'db_category', 'db_data') class ObjectCreateForm(forms.ModelForm): @@ -59,6 +48,7 @@ class ObjectEditForm(ObjectCreateForm): class ObjectDBAdmin(admin.ModelAdmin): + inlines = [ObjectTagInline, ObjectAttributeInline] list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path') list_display_links = ('id', 'db_key') ordering = ['db_player', 'db_typeclass_path', 'id'] @@ -88,7 +78,6 @@ class ObjectDBAdmin(admin.ModelAdmin): # ) #deactivated temporarily, they cause empty objects to be created in admin - inlines = [TagInline] # Custom modification to give two different forms wether adding or not. add_form = ObjectCreateForm @@ -135,4 +124,3 @@ class ObjectDBAdmin(admin.ModelAdmin): admin.site.register(ObjectDB, ObjectDBAdmin) -admin.site.register(Tag, TagAdmin) diff --git a/src/objects/models.py b/src/objects/models.py index 409c6aad0f..7ad13c661b 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -144,9 +144,9 @@ class ObjectDB(TypedObject): # make sure to sync the contents cache when initializing #_GA(self, "contents_update")() - def _at_db_player_presave(self): + def _at_db_player_postsave(self): """ - This hook is called automatically just before the player field is saved. + This hook is called automatically after the player field is saved. """ # we need to re-cache this for superusers to bypass. self.locks.cache_lock_bypass(self) diff --git a/src/objects/objects.py b/src/objects/objects.py index 1a7d09facf..dd0a978243 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -526,7 +526,6 @@ class Object(TypeClass): # commands may set this (create an item and you should be its # controller, for example) - dbref = self.dbobj.dbref self.locks.add(";".join([ "control:perm(Immortals)", # edit locks/permissions, delete "examine:perm(Builders)", # examine properties @@ -536,8 +535,7 @@ class Object(TypeClass): "get:all()", # pick up object "call:true()", # allow to call commands on this object "tell:perm(Wizards)", # allow emits to this object - # restricts puppeting of this object - "puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref])) + "puppet:pperm(Immortals)"])) # lock down puppeting only to staff by default def basetype_posthook_setup(self): """ diff --git a/src/players/admin.py b/src/players/admin.py index 279d1fff01..4dd89e684d 100644 --- a/src/players/admin.py +++ b/src/players/admin.py @@ -4,15 +4,12 @@ # from django import forms -#from django.db import models from django.conf import settings from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -#from django.contrib.admin import widgets from django.contrib.auth.forms import UserChangeForm, UserCreationForm -#from django.contrib.auth.models import User from src.players.models import PlayerDB -#from src.typeclasses.models import Attribute +from src.typeclasses.admin import AttributeInline, TagInline from src.utils import create @@ -22,19 +19,25 @@ class PlayerDBChangeForm(UserChangeForm): class Meta: model = PlayerDB - username = forms.RegexField(label="Username", - max_length=30, - regex=r'^[\w. @+-]+$', - widget=forms.TextInput(attrs={'size':'30'}), - error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, - help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") + username = forms.RegexField( + label="Username", + max_length=30, + regex=r'^[\w. @+-]+$', + widget=forms.TextInput( + attrs={'size': '30'}), + error_messages={ + 'invalid': "This value may contain only letters, spaces, numbers " + "and @/./+/-/_ characters."}, + help_text="30 characters or fewer. Letters, spaces, digits and " + "@/./+/-/_ only.") def clean_username(self): username = self.cleaned_data['username'] if username.upper() == self.instance.username.upper(): return username elif PlayerDB.objects.filter(username__iexact=username): - raise forms.ValidationError('A player with that name already exists.') + raise forms.ValidationError('A player with that name ' + 'already exists.') return self.cleaned_data['username'] @@ -43,75 +46,90 @@ class PlayerDBCreationForm(UserCreationForm): class Meta: model = PlayerDB - username = forms.RegexField(label="Username", - max_length=30, - regex=r'^[\w. @+-]+$', - widget=forms.TextInput(attrs={'size':'30'}), - error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, - help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") + username = forms.RegexField( + label="Username", + max_length=30, + regex=r'^[\w. @+-]+$', + widget=forms.TextInput( + attrs={'size': '30'}), + error_messages={ + 'invalid': "This value may contain only letters, spaces, numbers " + "and @/./+/-/_ characters."}, + help_text="30 characters or fewer. Letters, spaces, digits and " + "@/./+/-/_ only.") def clean_username(self): username = self.cleaned_data['username'] if PlayerDB.objects.filter(username__iexact=username): - raise forms.ValidationError('A player with that name already exists.') + raise forms.ValidationError('A player with that name already ' + 'exists.') return username -# # The Player editor -# class AttributeForm(forms.ModelForm): -# "Defines how to display the atttributes" -# class Meta: -# model = Attribute -# db_key = forms.CharField(label="Key", -# widget=forms.TextInput(attrs={'size':'15'})) -# db_value = forms.CharField(label="Value", -# widget=forms.Textarea(attrs={'rows':'2'})) - -# class AttributeInline(admin.TabularInline): -# "Inline creation of player attributes" -# model = Attribute -# extra = 0 -# form = AttributeForm -# fieldsets = ( -# (None, {'fields' : (('db_key', 'db_value'))}),) - class PlayerForm(forms.ModelForm): - "Defines how to display Players" - + """ + Defines how to display Players + """ class Meta: model = PlayerDB - db_key = forms.RegexField(label="Username", - initial="PlayerDummy", - max_length=30, - regex=r'^[\w. @+-]+$', - required=False, - widget=forms.TextInput(attrs={'size':'30'}), - error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, - help_text = "This should be the same as the connected Player's key name. 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") + db_key = forms.RegexField( + label="Username", + initial="PlayerDummy", + max_length=30, + regex=r'^[\w. @+-]+$', + required=False, + widget=forms.TextInput(attrs={'size': '30'}), + error_messages={ + 'invalid': "This value may contain only letters, spaces, numbers" + " and @/./+/-/_ characters."}, + help_text="This should be the same as the connected Player's key " + "name. 30 characters or fewer. Letters, spaces, digits and " + "@/./+/-/_ only.") - db_typeclass_path = forms.CharField(label="Typeclass", - initial=settings.BASE_PLAYER_TYPECLASS, - widget=forms.TextInput(attrs={'size':'78'}), - help_text="Required. Defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. Defaults to settings.BASE_PLAYER_TYPECLASS.") - #db_permissions = forms.CharField(label="Permissions", - # initial=settings.PERMISSION_PLAYER_DEFAULT, - # required=False, - # widget=forms.TextInput(attrs={'size':'78'}), - # help_text="In-game permissions. A comma-separated list of text strings checked by certain locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. A Player permission can be overloaded by the permissions of a controlled Character. Normal players use 'Players' by default.") - db_lock_storage = forms.CharField(label="Locks", - widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}), - required=False, - help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form type:lockfunction(args);type2:lockfunction2(args);...") - db_cmdset_storage = forms.CharField(label="cmdset", - initial=settings.CMDSET_PLAYER, - widget=forms.TextInput(attrs={'size':'78'}), - required=False, - help_text="python path to player cmdset class (set in settings.CMDSET_PLAYER by default)") + db_typeclass_path = forms.CharField( + label="Typeclass", + initial=settings.BASE_PLAYER_TYPECLASS, + widget=forms.TextInput( + attrs={'size': '78'}), + help_text="Required. Defines what 'type' of entity this is. This " + "variable holds a Python path to a module with a valid " + "Evennia Typeclass. Defaults to " + "settings.BASE_PLAYER_TYPECLASS.") + + db_permissions = forms.CharField( + label="Permissions", + initial=settings.PERMISSION_PLAYER_DEFAULT, + required=False, + widget=forms.TextInput( + attrs={'size': '78'}), + help_text="In-game permissions. A comma-separated list of text " + "strings checked by certain locks. They are often used for " + "hierarchies, such as letting a Player have permission " + "'Wizards', 'Builders' etc. A Player permission can be " + "overloaded by the permissions of a controlled Character. " + "Normal players use 'Players' by default.") + + db_lock_storage = forms.CharField( + label="Locks", + widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}), + required=False, + help_text="In-game lock definition string. If not given, defaults " + "will be used. This string should be on the form " + "type:lockfunction(args);type2:lockfunction2(args);...") + db_cmdset_storage = forms.CharField( + label="cmdset", + initial=settings.CMDSET_PLAYER, + widget=forms.TextInput(attrs={'size': '78'}), + required=False, + help_text="python path to player cmdset class (set in " + "settings.CMDSET_PLAYER by default)") class PlayerInline(admin.StackedInline): - "Inline creation of Player" + """ + Inline creation of Player + """ model = PlayerDB template = "admin/players/stacked.html" form = PlayerForm @@ -119,51 +137,80 @@ class PlayerInline(admin.StackedInline): ("In-game Permissions and Locks", {'fields': ('db_lock_storage',), #{'fields': ('db_permissions', 'db_lock_storage'), - 'description':"These are permissions/locks for in-game use. They are unrelated to website access rights."}), + 'description': "These are permissions/locks for in-game use. " + "They are unrelated to website access rights."}), ("In-game Player data", - {'fields':('db_typeclass_path', 'db_cmdset_storage'), - 'description':"These fields define in-game-specific properties for the Player object in-game."}), - ) + {'fields': ('db_typeclass_path', 'db_cmdset_storage'), + 'description': "These fields define in-game-specific properties " + "for the Player object in-game."})) extra = 1 max_num = 1 +class PlayerTagInline(TagInline): + model = PlayerDB.db_tags.through + + +class PlayerAttributeInline(AttributeInline): + model = PlayerDB.db_attributes.through + + class PlayerDBAdmin(BaseUserAdmin): - "This is the main creation screen for Users/players" + """ + This is the main creation screen for Users/players + """ list_display = ('username', 'email', 'is_staff', 'is_superuser') form = PlayerDBChangeForm add_form = PlayerDBCreationForm + inlines = [PlayerTagInline, PlayerAttributeInline] fieldsets = ( (None, {'fields': ('username', 'password', 'email')}), - ('Website profile', {'fields': ('first_name', 'last_name'), - 'description': "These are not used in the default system."}), - ('Website dates', {'fields': ('last_login', 'date_joined'), - 'description': 'Relevant only to the website.'}), - ('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', - 'user_permissions', 'groups'), - 'description': "These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights."}), - ('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'), - 'description': 'These are attributes that are more relevant to gameplay.'})) - #('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'), - # 'description': 'These are attributes that are more relevant to gameplay.'})) + ('Website profile', { + 'fields': ('first_name', 'last_name'), + 'description': "These are not used " + "in the default system."}), + ('Website dates', { + 'fields': ('last_login', 'date_joined'), + 'description': 'Relevant only to the website.'}), + ('Website Permissions', { + 'fields': ('is_active', 'is_staff', 'is_superuser', + 'user_permissions', 'groups'), + 'description': "These are permissions/permission groups for " + "accessing the admin site. They are unrelated to " + "in-game access rights."}), + ('Game Options', { + 'fields': ('db_typeclass_path', 'db_cmdset_storage', + 'db_lock_storage'), + 'description': 'These are attributes that are more relevant ' + 'to gameplay.'})) + # ('Game Options', {'fields': ( + # 'db_typeclass_path', 'db_cmdset_storage', + # 'db_permissions', 'db_lock_storage'), + # 'description': 'These are attributes that are ' + # 'more relevant to gameplay.'})) add_fieldsets = ( (None, {'fields': ('username', 'password1', 'password2', 'email'), - 'description':"These account details are shared by the admin system and the game."},),) + 'description': "These account details are shared by the admin " + "system and the game."},),) # TODO! Remove User reference! def save_formset(self, request, form, formset, change): - "Run all hooks on the player object" + """ + Run all hooks on the player object + """ super(PlayerDBAdmin, self).save_formset(request, form, formset, change) userobj = form.instance userobj.name = userobj.username if not change: - #uname, passwd, email = str(request.POST.get(u"username")), \ - # str(request.POST.get(u"password1")), str(request.POST.get(u"email")) - typeclass = str(request.POST.get(u"playerdb_set-0-db_typeclass_path")) + # uname, passwd, email = str(request.POST.get(u"username")), \ + # str(request.POST.get(u"password1")), \ + # str(request.POST.get(u"email")) + typeclass = str(request.POST.get( + u"playerdb_set-0-db_typeclass_path")) create.create_player("", "", "", user=userobj, typeclass=typeclass, diff --git a/src/scripts/admin.py b/src/scripts/admin.py index 13ff1cbdf7..76e2562d88 100644 --- a/src/scripts/admin.py +++ b/src/scripts/admin.py @@ -2,16 +2,18 @@ # This sets up how models are displayed # in the web admin interface. # +from src.typeclasses.admin import AttributeInline, TagInline -from src.typeclasses.models import Attribute from src.scripts.models import ScriptDB from django.contrib import admin -class AttributeInline(admin.TabularInline): - model = Attribute - fields = ('db_key', 'db_value') - max_num = 1 +class ScriptTagInline(TagInline): + model = ScriptDB.db_tags.through + + +class ScriptAttributeInline(AttributeInline): + model = ScriptDB.db_attributes.through class ScriptDBAdmin(admin.ModelAdmin): @@ -32,7 +34,7 @@ class ScriptDBAdmin(admin.ModelAdmin): 'db_repeats', 'db_start_delay', 'db_persistent', 'db_obj')}), ) - #inlines = [AttributeInline] + inlines = [ScriptTagInline, ScriptAttributeInline] admin.site.register(ScriptDB, ScriptDBAdmin) diff --git a/src/server/caches.py b/src/server/caches.py index 3405b0aa53..c55d50df3d 100644 --- a/src/server/caches.py +++ b/src/server/caches.py @@ -80,28 +80,28 @@ def hashid(obj, suffix=""): #------------------------------------------------------------ # callback to field pre_save signal (connected in src.server.server) -def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs): - """ - Called at the beginning of the field save operation. The save method - must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this - function. Its main purpose is to allow to plug-in a save handler and oob - handlers. - """ - if raw: - return - if update_fields: - # this is a list of strings at this point. We want field objects - update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) - else: - # meta.fields are already field objects; get them all - update_fields = _GA(_GA(instance, "_meta"), "fields") - for field in update_fields: - fieldname = field.name - handlername = "_at_%s_presave" % fieldname - handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None - if callable(handler): - handler() +#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs): +# """ +# Called at the beginning of the field save operation. The save method +# must be called with the update_fields keyword in order to be most efficient. +# This method should NOT save; rather it is the save() that triggers this +# function. Its main purpose is to allow to plug-in a save handler and oob +# handlers. +# """ +# if raw: +# return +# if update_fields: +# # this is a list of strings at this point. We want field objects +# update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) +# else: +# # meta.fields are already field objects; get them all +# update_fields = _GA(_GA(instance, "_meta"), "fields") +# for field in update_fields: +# fieldname = field.name +# handlername = "_at_%s_presave" % fieldname +# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None +# if callable(handler): +# handler() def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index 031abf56a8..a7af784edd 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -180,42 +180,6 @@ def start_game_time(): gametime.init_gametime() -def create_admin_media_links(): - """ - This traverses to src/web/media and tries to create a symbolic - link to the django media files from within the MEDIA_ROOT. - These are files we normally don't - want to mess with (use templates to customize the admin - look). Linking is needed since the Twisted webserver otherwise has no - notion of where the default files are - and we cannot hard-code it - since the django install may be at different locations depending - on system. - """ - import django - import os - - if django.get_version() < 1.4: - dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media') - else: - dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'static', 'admin') - apath = os.path.join(settings.ADMIN_MEDIA_ROOT) - if os.path.isdir(apath): - print " ADMIN_MEDIA_ROOT already exists. Ignored." - return - if os.name == 'nt': - print " Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode)." - os.mkdir(apath) - os.system('xcopy "%s" "%s" /e /q /c' % (dpath, apath)) - if os.name == 'posix': - try: - os.symlink(dpath, apath) - print " Admin-media symlinked to ADMIN_MEDIA_ROOT." - except OSError, e: - print " There was an error symlinking Admin-media to ADMIN_MEDIA_ROOT:\n %s\n -> \n %s\n (%s)\n If you see issues, link manually." % (dpath, apath, e) - else: - print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT." - - def at_initial_setup(): """ Custom hook for users to overload some or all parts of the initial @@ -269,7 +233,6 @@ def handle_setup(last_step): create_channels, create_system_scripts, start_game_time, - create_admin_media_links, at_initial_setup, reset_server ] diff --git a/src/server/oob_cmds.py b/src/server/oob_cmds.py new file mode 100644 index 0000000000..42a762c3bf --- /dev/null +++ b/src/server/oob_cmds.py @@ -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" + } diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py deleted file mode 100644 index 9bafa9b79c..0000000000 --- a/src/server/oob_msdp.py +++ /dev/null @@ -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) - diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index d4f5eeda77..1dc4aca882 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -36,7 +36,6 @@ messages. from inspect import isfunction from twisted.internet.defer import inlineCallbacks -from twisted.internet.task import LoopingCall from django.conf import settings from src.server.models import ServerConfig from src.server.sessionhandler import SESSIONS @@ -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() diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py index c40bf5dedc..8c4a812199 100644 --- a/src/server/portal/portal.py +++ b/src/server/portal/portal.py @@ -42,20 +42,21 @@ TELNET_PORTS = settings.TELNET_PORTS SSL_PORTS = settings.SSL_PORTS SSH_PORTS = settings.SSH_PORTS WEBSERVER_PORTS = settings.WEBSERVER_PORTS -WEBSOCKET_PORTS = settings.WEBSOCKET_PORTS +WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT TELNET_INTERFACES = settings.TELNET_INTERFACES SSL_INTERFACES = settings.SSL_INTERFACES SSH_INTERFACES = settings.SSH_INTERFACES WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES -WEBSOCKET_INTERFACES = settings.WEBSOCKET_INTERFACES +WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE +WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED -WEBSOCKET_ENABLED = settings.WEBSOCKET_ENABLED and WEBSOCKET_PORTS and WEBSOCKET_INTERFACES +WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE AMP_HOST = settings.AMP_HOST AMP_PORT = settings.AMP_PORT @@ -257,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) diff --git a/src/server/portal/webclient.py b/src/server/portal/webclient.py index c5f34558ed..74c32c6a4f 100644 --- a/src/server/portal/webclient.py +++ b/src/server/portal/webclient.py @@ -18,6 +18,7 @@ found on http://localhost:8000/webclient.) """ import time import json + from hashlib import md5 from twisted.web import server, resource @@ -32,7 +33,6 @@ from src.server import session SERVERNAME = settings.SERVERNAME ENCODINGS = settings.ENCODINGS - # defining a simple json encoder for returning # django data to the client. Might need to # extend this if one wants to send more diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket_client.py similarity index 82% rename from src/server/portal/websocket.py rename to src/server/portal/websocket_client.py index e457368db3..d3a7bd553a 100644 --- a/src/server/portal/websocket.py +++ b/src/server/portal/websocket_client.py @@ -1,8 +1,9 @@ """ -Websockets Protocol +Websocket-webclient -This implements WebSockets (http://en.wikipedia.org/wiki/WebSocket) -by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). +This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket) +by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is +used together with src/web/media/javascript/evennia_websocket_webclient.js. Thanks to Ricard Pillosu whose Evennia plugin inspired this module. @@ -10,13 +11,13 @@ Communication over the websocket interface is done with normal text communication. A special case is OOB-style communication; to do this the client must send data on the following form: - OOB{oobfunc:[[args], {kwargs}], ...} + OOB{"func1":[args], "func2":[args], ...} -where the tuple/list is sent json-encoded. The initial OOB-prefix +where the dict is JSON encoded. The initial OOB-prefix is used to identify this type of communication, all other data is considered plain text (command input). -Example of call from javascript client: +Example of call from a javascript client: websocket = new WeSocket("ws://localhost:8021") var msg1 = "WebSocket Test" @@ -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) diff --git a/src/server/server.py b/src/server/server.py index 9a0c7f1ee5..5e7166ce1e 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -33,9 +33,9 @@ from src.server.sessionhandler import SESSIONS # setting up server-side field cache from django.db.models.signals import post_save -from src.server.caches import field_pre_save +from src.server.caches import field_post_save #pre_save.connect(field_pre_save, dispatch_uid="fieldcache") -post_save.connect(field_pre_save, dispatch_uid="fieldcache") +post_save.connect(field_post_save, dispatch_uid="fieldcache") #from src.server.caches import post_attr_update #from django.db.models.signals import m2m_changed @@ -411,6 +411,8 @@ if WEBSERVER_ENABLED: web_root = DjangoWebRoot(threads) # point our media resources to url /media web_root.putChild("media", static.File(settings.MEDIA_ROOT)) + # point our static resources to url /static + web_root.putChild("static", static.File(settings.STATIC_ROOT)) web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) for proxyport, serverport in WEBSERVER_PORTS: diff --git a/src/server/serversession.py b/src/server/serversession.py index ce04bfd0fa..00551f4b42 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -206,6 +206,7 @@ class ServerSession(Session): if not _OOB_HANDLER: from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None)) + #print "session.data_in: oobstruct:",oobstruct for (funcname, args, kwargs) in oobstruct: if funcname: _OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs) diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index bfb7efb313..2dc3edd152 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -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),) diff --git a/src/settings_default.py b/src/settings_default.py index 68ed71c333..76dc73ad0e 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -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" diff --git a/src/typeclasses/admin.py b/src/typeclasses/admin.py new file mode 100644 index 0000000000..525d702f1c --- /dev/null +++ b/src/typeclasses/admin.py @@ -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 '%s' % ( + reverse("admin:typeclasses_attribute_change", + args=[instance.attribute.id]), + instance.attribute.db_key) + + key.allow_tags = True + + def value(self, instance): + if not instance.id: + return "Not yet set or saved." + return instance.attribute.db_value + + def strvalue(self, instance): + if not instance.id: + return "Not yet set or saved." + return instance.attribute.db_strvalue + + +class AttributeAdmin(ModelAdmin): + """ + Defines how to display the attributes + """ + search_fields = ('db_key', 'db_strvalue', 'db_value') + list_display = ('db_key', 'db_strvalue', 'db_value') + permitted_types = ('str', 'int', 'float', 'NoneType', 'bool') + + fields = ('db_key', 'db_value', 'db_strvalue', 'db_category', + 'db_lock_storage', 'db_model', 'db_attrtype') + + def get_readonly_fields(self, request, obj=None): + if obj.db_value.__class__.__name__ not in self.permitted_types: + return ['db_value'] + return [] + +admin.site.register(Attribute, AttributeAdmin) +admin.site.register(Tag, TagAdmin) \ No newline at end of file diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index c4b03a743d..2d96558fae 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -34,6 +34,7 @@ import weakref from django.db import models from django.core.exceptions import ObjectDoesNotExist from django.conf import settings +from django.db.models import Q from django.utils.encoding import smart_str from django.contrib.contenttypes.models import ContentType @@ -46,7 +47,8 @@ from src.server.models import ServerConfig from src.typeclasses import managers from src.locks.lockhandler import LockHandler from src.utils import logger -from src.utils.utils import make_iter, is_iter, to_str, inherits_from, LazyLoadHandler +from src.utils.utils import ( + make_iter, is_iter, to_str, inherits_from, LazyLoadHandler) from src.utils.dbserialize import to_pickle, from_pickle from src.utils.picklefield import PickledObjectField @@ -69,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." diff --git a/src/utils/ansi.py b/src/utils/ansi.py index 090cd37596..72b6f8006a 100644 --- a/src/utils/ansi.py +++ b/src/utils/ansi.py @@ -223,8 +223,11 @@ class ANSIParser(object): (r'%cn', ANSI_NORMAL), (r'%ch', ANSI_HILITE), (r'%r', ANSI_RETURN), + (r'%R', ANSI_RETURN), (r'%t', ANSI_TAB), + (r'%T', ANSI_TAB), (r'%b', ANSI_SPACE), + (r'%B', ANSI_SPACE), (r'%cf', ANSI_BLINK), # annoying and not supported by all clients (r'%ci', ANSI_INVERSE), diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py index dc33464ee3..3c59ad31f9 100644 --- a/src/utils/picklefield.py +++ b/src/utils/picklefield.py @@ -28,15 +28,21 @@ Pickle field implementation for Django. Modified for Evennia by Griatch. """ +from ast import literal_eval from copy import deepcopy from base64 import b64encode, b64decode from zlib import compress, decompress #import six # this is actually a pypy component, not in default syslib import django +from django.core.exceptions import ValidationError from django.db import models # django 1.5 introduces force_text instead of force_unicode +from django.forms import CharField, Textarea +from django.forms.util import flatatt +from django.utils.html import format_html + try: from django.utils.encoding import force_text except ImportError: @@ -120,6 +126,45 @@ def _get_subfield_superclass(): #return six.with_metaclass(models.SubfieldBase, models.Field) +class PickledWidget(Textarea): + def render(self, name, value, attrs=None): + value = repr(value) + try: + literal_eval(value) + except ValueError: + return value + + final_attrs = self.build_attrs(attrs, name=name) + return format_html('\r\n{1}', + flatatt(final_attrs), + force_text(value)) + + +class PickledFormField(CharField): + widget = PickledWidget + default_error_messages = dict(CharField.default_error_messages) + default_error_messages['invalid'] = ( + "This is not a Python Literal. You can store things like strings, " + "integers, or floats, but you must do it by typing them as you would " + "type them in the Python Interpreter. For instance, strings must be " + "surrounded by quote marks. We have converted it to a string for your " + "convenience. If it is acceptable, please hit save again.") + + def __init__(self, *args, **kwargs): + # This needs to fall through to literal_eval. + kwargs['required'] = False + super(PickledFormField, self).__init__(*args, **kwargs) + + def clean(self, value): + if value == '': + # Field was left blank. Make this None. + value = 'None' + try: + return literal_eval(value) + except (ValueError, 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) diff --git a/src/web/news/admin.py b/src/web/news/admin.py deleted file mode 100644 index 9e48748f01..0000000000 --- a/src/web/news/admin.py +++ /dev/null @@ -1,17 +0,0 @@ -# -# This makes the news model visible in the admin web interface -# so one can add/edit/delete news items etc. -# - -from django.contrib import admin -from src.web.news.models import NewsTopic, NewsEntry - -class NewsTopicAdmin(admin.ModelAdmin): - list_display = ('name', 'icon') -admin.site.register(NewsTopic, NewsTopicAdmin) - -class NewsEntryAdmin(admin.ModelAdmin): - list_display = ('title', 'author', 'topic', 'date_posted') - list_filter = ('topic',) - search_fields = ['title'] -admin.site.register(NewsEntry, NewsEntryAdmin) diff --git a/src/web/news/models.py b/src/web/news/models.py deleted file mode 100755 index d89d3362df..0000000000 --- a/src/web/news/models.py +++ /dev/null @@ -1,47 +0,0 @@ -# -# This module implements a simple news entry system -# for the evennia website. One needs to use the -# admin interface to add/edit/delete entries. -# - -from django.db import models -from django.contrib.auth import get_user_model - -User = get_user_model() - -class NewsTopic(models.Model): - """ - Represents a news topic. - """ - name = models.CharField(max_length=75, unique=True) - description = models.TextField(blank=True) - icon = models.ImageField(upload_to='newstopic_icons', - default='newstopic_icons/default.png', - blank=True, help_text="Image for the news topic.") - - def __str__(self): - try: - return self.name - except: - return "Invalid" - - class Meta: - ordering = ['name'] - -class NewsEntry(models.Model): - """ - An individual news entry. - """ - author = models.ForeignKey(User, related_name='author') - title = models.CharField(max_length=255) - body = models.TextField() - topic = models.ForeignKey(NewsTopic, related_name='newstopic') - date_posted = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.title - - class Meta: - ordering = ('-date_posted',) - verbose_name_plural = "News entries" - diff --git a/src/web/news/urls.py b/src/web/news/urls.py deleted file mode 100755 index 09c4a3b6fe..0000000000 --- a/src/web/news/urls.py +++ /dev/null @@ -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\d+)/$', 'show_news'), - (r'^archive/$', 'news_archive'), - (r'^search/$', 'search_form'), - (r'^search/results/$', 'search_results'), -) diff --git a/src/web/news/views.py b/src/web/news/views.py deleted file mode 100755 index 54f2caabab..0000000000 --- a/src/web/news/views.py +++ /dev/null @@ -1,129 +0,0 @@ - -""" -This is a very simple news application, with most of the expected features -like news-categories/topics and searchable archives. - -""" - -from django.views.generic import ListView -from django.shortcuts import render_to_response, get_object_or_404 -from django.template import RequestContext -from django.conf import settings -from django.http import HttpResponseRedirect -from django.contrib.auth.models import User -from django import forms -from django.db.models import Q - -from src.web.news.models import NewsTopic, NewsEntry - -# The sidebar text to be included as a variable on each page. There's got to -# be a better, cleaner way to include this on every page. -sidebar = """ -

This page’s menu:

- -""" - -class SearchForm(forms.Form): - """ - Class to represent a news search form under Django's newforms. This is used - to validate the input on the search_form view, as well as the search_results - view when we're picking the query out of GET. This makes searching safe - via the search form or by directly inputing values via GET key pairs. - """ - search_terms = forms.CharField(max_length=100, min_length=3, required=True) - -def show_news(request, entry_id): - """ - Show an individual news entry. Display some basic information along with - the title and content. - """ - news_entry = get_object_or_404(NewsEntry, id=entry_id) - - pagevars = { - "page_title": "News Entry", - "news_entry": news_entry, - "sidebar": sidebar - } - - context_instance = RequestContext(request) - return render_to_response('news/show_entry.html', pagevars, context_instance) - -def news_archive(request): - """ - Shows an archive of news entries. - - TODO: Expand this a bit to allow filtering by month/year. - """ - news_entries = NewsEntry.objects.all().order_by('-date_posted') - # TODO: Move this to either settings.py or the SQL configuration. - entries_per_page = 15 - - pagevars = { - "page_title": "News Archive", - "browse_url": "/news/archive", - "sidebar": sidebar - } - view = ListView.as_view(queryset=news_entries) - return view(request, template_name='news/archive.html', \ - extra_context=pagevars, paginate_by=entries_per_page) - -def search_form(request): - """ - Render the news search form. Don't handle much validation at all. If the - user enters a search term that meets the minimum, send them on their way - to the results page. - """ - if request.method == 'GET': - # A GET request was sent to the search page, load the value and - # validate it. - search_form = SearchForm(request.GET) - if search_form.is_valid(): - # If the input is good, send them to the results page with the - # query attached in GET variables. - return HttpResponseRedirect('/news/search/results/?search_terms='+ search_form.cleaned_data['search_terms']) - else: - # Brand new search, nothing has been sent just yet. - search_form = SearchForm() - - pagevars = { - "page_title": "Search News", - "search_form": search_form, - "debug": settings.DEBUG, - "sidebar": sidebar - } - - context_instance = RequestContext(request) - return render_to_response('news/search_form.html', pagevars, context_instance) - -def search_results(request): - """ - Shows an archive of news entries. Use the generic news browsing template. - """ - # TODO: Move this to either settings.py or the SQL configuration. - entries_per_page = 15 - - # Load the form values from GET to validate against. - search_form = SearchForm(request.GET) - # You have to call is_valid() or cleaned_data won't be populated. - valid_search = search_form.is_valid() - # This is the safe data that we can pass to queries without huge worry of - # badStuff(tm). - cleaned_get = search_form.cleaned_data - - # Perform searches that match the title and contents. - # TODO: Allow the user to specify what to match against and in what - # topics/categories. - news_entries = NewsEntry.objects.filter(Q(title__contains=cleaned_get['search_terms']) | Q(body__contains=cleaned_get['search_terms'])) - - pagevars = { - "game_name": settings.SERVERNAME, - "page_title": "Search Results", - "searchtext": cleaned_get['search_terms'], - "browse_url": "/news/search/results", - "sidebar": sidebar - } - view = ListView.as_view(queryset=news_entries) - return view(request, news_entries, template_name='news/archive.html', extra_context=pagevars, paginate_by=entries_per_page) diff --git a/src/web/media/css/prosimii-print.css b/src/web/static/evennia_general/css/prosimii-print.css similarity index 100% rename from src/web/media/css/prosimii-print.css rename to src/web/static/evennia_general/css/prosimii-print.css diff --git a/src/web/media/css/prosimii-screen-alt.css b/src/web/static/evennia_general/css/prosimii-screen-alt.css similarity index 100% rename from src/web/media/css/prosimii-screen-alt.css rename to src/web/static/evennia_general/css/prosimii-screen-alt.css diff --git a/src/web/media/css/prosimii-screen.css b/src/web/static/evennia_general/css/prosimii-screen.css similarity index 100% rename from src/web/media/css/prosimii-screen.css rename to src/web/static/evennia_general/css/prosimii-screen.css diff --git a/src/web/media/images/LICENCE b/src/web/static/evennia_general/images/LICENCE similarity index 100% rename from src/web/media/images/LICENCE rename to src/web/static/evennia_general/images/LICENCE diff --git a/src/web/media/images/evennia_logo.png b/src/web/static/evennia_general/images/evennia_logo.png similarity index 100% rename from src/web/media/images/evennia_logo.png rename to src/web/static/evennia_general/images/evennia_logo.png diff --git a/src/web/media/images/evennia_logo_small.png b/src/web/static/evennia_general/images/evennia_logo_small.png similarity index 100% rename from src/web/media/images/evennia_logo_small.png rename to src/web/static/evennia_general/images/evennia_logo_small.png diff --git a/src/web/media/images/favicon.ico b/src/web/static/evennia_general/images/favicon.ico similarity index 100% rename from src/web/media/images/favicon.ico rename to src/web/static/evennia_general/images/favicon.ico diff --git a/src/web/templates/admin/base_site.html b/src/web/templates/admin/base_site.html deleted file mode 100644 index 578579e79b..0000000000 --- a/src/web/templates/admin/base_site.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "admin/base.html" %} -{% load i18n %} - -{% block title %}{{ title }} | {% trans 'Evennia site admin' %}{% endblock %} - -{% block branding %} -

{% trans 'Evennia database administration' %} - (Back)

-{% endblock %} - -{% block nav-global %}{% endblock %} diff --git a/src/web/templates/admin/index.html b/src/web/templates/admin/index.html deleted file mode 100644 index a7487cd49d..0000000000 --- a/src/web/templates/admin/index.html +++ /dev/null @@ -1,184 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_static %} - -{% block extrastyle %}{{ block.super }}{% endblock %} - -{% block coltype %}colMS{% endblock %} - -{% block bodyclass %}dashboard{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -
- -{% if app_list %} - - {% for app in app_list %} - - {% if app.name in evennia_userapps %} - - {% if app.name == 'Players' %} -

Admin

-

Players are the out-of-character representation of a - game account. A Player can potentially control any number of - in-game character Objects (depending on game).

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

Game entities

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

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

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

Website

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

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

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

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

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

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

- {% else %} -

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

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

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

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

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

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

{% trans 'Filter' %}

- {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} -
- {% endif %} - {% endblock %} - -
{% csrf_token %} - {% if cl.formset %} -
{{ cl.formset.management_form }}
- {% endif %} - - {% block result_list %} - {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} - {% result_list cl %} - {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} - {% endblock %} - {% block pagination %}{% pagination cl %}{% endblock %} -
-
-
-{% endblock %} diff --git a/src/web/templates/admin/players/stacked.html b/src/web/templates/admin/players/stacked.html deleted file mode 100644 index 617ca9987e..0000000000 --- a/src/web/templates/admin/players/stacked.html +++ /dev/null @@ -1,82 +0,0 @@ -{% load i18n admin_static %} -
- -{{ inline_admin_formset.formset.management_form }} -{{ inline_admin_formset.formset.non_form_errors }} - -{% for inline_admin_form in inline_admin_formset %}
- - {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} - {% for fieldset in inline_admin_form %} - {% include "admin/includes/fieldset.html" %} - {% endfor %} - {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} - {{ inline_admin_form.fk_field.field }} -
{% endfor %} -
- - diff --git a/src/web/templates/prosimii/base.html b/src/web/templates/prosimii/base.html index 76e241dc3f..4c260f8c2e 100644 --- a/src/web/templates/prosimii/base.html +++ b/src/web/templates/prosimii/base.html @@ -1,3 +1,4 @@ +{% load staticfiles %} @@ -8,12 +9,12 @@ {% if sidebar %} - + {% else %} - + {% endif %} - - + + {% block header_ext %} {% endblock %} @@ -34,7 +35,7 @@
-

{{game_name}}

+

{{game_name}}

{{game_slogan}}   @@ -45,13 +46,13 @@
@@ -61,9 +62,9 @@ Home | About | Documentation | - Admin Interface + Admin Interface {% if webclient_enabled %} - | Play Online + | Play Online {% endif %}
@@ -87,7 +88,7 @@ title="Other designs by haran">haran. Powered by Evennia. -
+
diff --git a/src/web/templates/prosimii/evennia_admin.html b/src/web/templates/prosimii/evennia_admin.html new file mode 100644 index 0000000000..ab7b3a42a2 --- /dev/null +++ b/src/web/templates/prosimii/evennia_admin.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block content %} +

Admin

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

Players

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

Objects

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

Scripts

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

Channels

+ Channels are used for player communications. + +

Help Topics

+ +

If you are an advanced user who needs access to the raw Django Admin, it is available here. + You can make this the default my changing EVENNIA_ADMIN to False in settings.py and reload.

+{% endblock content %} \ No newline at end of file diff --git a/src/web/templates/prosimii/index.html b/src/web/templates/prosimii/index.html index 2594b742f3..2c7aa752a4 100644 --- a/src/web/templates/prosimii/index.html +++ b/src/web/templates/prosimii/index.html @@ -14,11 +14,11 @@

Welcome to your new installation of Evennia, your friendly neighborhood next-generation MUD development system and server. You are looking at Evennia's web presence, which can be expanded to a full-fledged site as - needed. Through the admin interface you can view and edit the + needed. Through the admin interface you can view and edit the database without logging into the game. {% if webclient_enabled %} You can also connect to the game directly from your browser using our - online client!

+ online client!

{% endif %} For more info, take your time to peruse our extensive online documentation. @@ -28,21 +28,6 @@ mailing list or to come say hi in the developer chatroom. If you find bugs, please report them to our Issue tracker.

- - - -

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

{{page_title}}

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

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

-

{{entry.body|truncatewords:80}}

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

Search News

-

Enter a search term or phrase to search by. Matches will be made against - news titles and their contents. Searches must be at least three characters - long.

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

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

-

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

-

{{news_entry.body}}

-{% endblock %} diff --git a/src/web/urls.py b/src/web/urls.py index 5ca3bf8a41..ea9e1576bc 100755 --- a/src/web/urls.py +++ b/src/web/urls.py @@ -6,8 +6,8 @@ # http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3 # -from django.conf.urls import * from django.conf import settings +from django.conf.urls import url, include from django.contrib import admin from django.views.generic import RedirectView @@ -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.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), - ) + urlpatterns.extend([ + url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), + url(r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT}) + ]) diff --git a/src/web/backends.py b/src/web/utils/backends.py similarity index 100% rename from src/web/backends.py rename to src/web/utils/backends.py diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py index 2b9304e94a..b901e856e3 100644 --- a/src/web/utils/general_context.py +++ b/src/web/utils/general_context.py @@ -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 } diff --git a/src/web/website/views.py b/src/web/views.py similarity index 76% rename from src/web/website/views.py rename to src/web/views.py index a0af399ddc..078af83045 100644 --- a/src/web/website/views.py +++ b/src/web/views.py @@ -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) \ No newline at end of file diff --git a/src/web/media/css/webclient.css b/src/web/webclient/static/webclient/css/webclient.css similarity index 100% rename from src/web/media/css/webclient.css rename to src/web/webclient/static/webclient/css/webclient.css diff --git a/src/web/media/javascript/evennia_webclient.js b/src/web/webclient/static/webclient/js/evennia_webclient.js similarity index 100% rename from src/web/media/javascript/evennia_webclient.js rename to src/web/webclient/static/webclient/js/evennia_webclient.js diff --git a/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js b/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js new file mode 100644 index 0000000000..b63b44b9ae --- /dev/null +++ b/src/web/webclient/static/webclient/js/evennia_websocket_webclient.js @@ -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( + "
"+ msg +"
"); + // 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); +}); diff --git a/src/web/templates/prosimii/webclient.html b/src/web/webclient/templates/webclient.html similarity index 52% rename from src/web/templates/prosimii/webclient.html rename to src/web/webclient/templates/webclient.html index 313736329e..b28aa204cd 100644 --- a/src/web/templates/prosimii/webclient.html +++ b/src/web/webclient/templates/webclient.html @@ -1,4 +1,5 @@ +{% load staticfiles %} @@ -6,16 +7,29 @@ Evennia web MUD client - + - + + - + - - + {% if websocket_enabled %} + + {% else %} + + + {% endif %} @@ -38,7 +52,7 @@
Logged in Players: {{num_players_connected}}
-
+
diff --git a/src/web/webclient/urls.py b/src/web/webclient/urls.py index 03f2595d2e..ba5cdf8186 100644 --- a/src/web/webclient/urls.py +++ b/src/web/webclient/urls.py @@ -4,5 +4,5 @@ webpage 'application'. """ from django.conf.urls import * -urlpatterns = patterns('', - url(r'^$', 'src.web.webclient.views.webclient'),) +urlpatterns = [ + url(r'^$', 'src.web.webclient.views.webclient', name="index")] diff --git a/src/web/webclient/views.py b/src/web/webclient/views.py index 3d097b6935..c8ea3c82ab 100644 --- a/src/web/webclient/views.py +++ b/src/web/webclient/views.py @@ -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) diff --git a/src/web/website/__init__.py b/src/web/website/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/web/website/models.py b/src/web/website/models.py deleted file mode 100644 index 89e59e2200..0000000000 --- a/src/web/website/models.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# Define database entities for the app. -# - -from django.db import models - - diff --git a/src/web/website/urls.py b/src/web/website/urls.py deleted file mode 100644 index 8bb4bd811e..0000000000 --- a/src/web/website/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -This structures the (simple) structure of the -webpage 'application'. -""" - -from django.conf.urls import * - -urlpatterns = patterns('src.web.website.views', - (r'^$', 'page_index'), -)