From ca3f92acd0031a656da70c98dc8f69b8cbb1f460 Mon Sep 17 00:00:00 2001 From: Kelketek Rritaa Date: Sat, 28 Jun 2014 16:16:09 -0500 Subject: [PATCH] Admin interface greatly improved. Support for editing Attributes added. Resolves #503. Resolves #201. --- src/comms/admin.py | 10 ++ src/objects/admin.py | 22 +--- src/players/admin.py | 219 ++++++++++++++++++++++++--------------- src/scripts/admin.py | 14 +-- src/typeclasses/admin.py | 64 ++++++++++++ src/utils/picklefield.py | 49 ++++++++- 6 files changed, 268 insertions(+), 110 deletions(-) create mode 100644 src/typeclasses/admin.py 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/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/typeclasses/admin.py b/src/typeclasses/admin.py new file mode 100644 index 0000000000..72d8e90235 --- /dev/null +++ b/src/typeclasses/admin.py @@ -0,0 +1,64 @@ +from django.contrib import admin +from django.contrib.admin import ModelAdmin +from django.core.urlresolvers import reverse +from django.forms import Textarea +from src.typeclasses.models import Attribute, Tag + + +class PickledWidget(Textarea): + pass + + +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 = 3 + #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') + +admin.site.register(Attribute, AttributeAdmin) +admin.site.register(Tag, TagAdmin) \ No newline at end of file diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py index dc33464ee3..d7b2d409cd 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: + 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)