Admin interface greatly improved. Support for editing Attributes added.

Resolves #503. Resolves #201.
This commit is contained in:
Kelketek Rritaa 2014-06-28 16:16:09 -05:00
parent fa20190467
commit ca3f92acd0
6 changed files with 268 additions and 110 deletions

View file

@ -5,6 +5,15 @@
from django.contrib import admin from django.contrib import admin
from src.comms.models import ChannelDB 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): class MsgAdmin(admin.ModelAdmin):
@ -21,6 +30,7 @@ class MsgAdmin(admin.ModelAdmin):
class ChannelAdmin(admin.ModelAdmin): class ChannelAdmin(admin.ModelAdmin):
inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions") list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions")
list_display_links = ("id", 'db_key') list_display_links = ("id", 'db_key')
ordering = ["db_key"] ordering = ["db_key"]

View file

@ -6,27 +6,16 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import admin 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 from src.objects.models import ObjectDB
class AttributeInline(admin.TabularInline): class ObjectAttributeInline(AttributeInline):
# This class is currently not used, because PickleField objects are model = ObjectDB.db_attributes.through
# 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 TagInline(admin.TabularInline): class ObjectTagInline(TagInline):
model = ObjectDB.db_tags.through 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): class ObjectCreateForm(forms.ModelForm):
@ -59,6 +48,7 @@ class ObjectEditForm(ObjectCreateForm):
class ObjectDBAdmin(admin.ModelAdmin): class ObjectDBAdmin(admin.ModelAdmin):
inlines = [ObjectTagInline, ObjectAttributeInline]
list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path') list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path')
list_display_links = ('id', 'db_key') list_display_links = ('id', 'db_key')
ordering = ['db_player', 'db_typeclass_path', 'id'] 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 #deactivated temporarily, they cause empty objects to be created in admin
inlines = [TagInline]
# Custom modification to give two different forms wether adding or not. # Custom modification to give two different forms wether adding or not.
add_form = ObjectCreateForm add_form = ObjectCreateForm
@ -135,4 +124,3 @@ class ObjectDBAdmin(admin.ModelAdmin):
admin.site.register(ObjectDB, ObjectDBAdmin) admin.site.register(ObjectDB, ObjectDBAdmin)
admin.site.register(Tag, TagAdmin)

View file

@ -4,15 +4,12 @@
# #
from django import forms from django import forms
#from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 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.forms import UserChangeForm, UserCreationForm
#from django.contrib.auth.models import User
from src.players.models import PlayerDB from src.players.models import PlayerDB
#from src.typeclasses.models import Attribute from src.typeclasses.admin import AttributeInline, TagInline
from src.utils import create from src.utils import create
@ -22,19 +19,25 @@ class PlayerDBChangeForm(UserChangeForm):
class Meta: class Meta:
model = PlayerDB model = PlayerDB
username = forms.RegexField(label="Username", username = forms.RegexField(
label="Username",
max_length=30, max_length=30,
regex=r'^[\w. @+-]+$', regex=r'^[\w. @+-]+$',
widget=forms.TextInput(attrs={'size':'30'}), widget=forms.TextInput(
error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, attrs={'size': '30'}),
help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") 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): def clean_username(self):
username = self.cleaned_data['username'] username = self.cleaned_data['username']
if username.upper() == self.instance.username.upper(): if username.upper() == self.instance.username.upper():
return username return username
elif PlayerDB.objects.filter(username__iexact=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'] return self.cleaned_data['username']
@ -43,75 +46,90 @@ class PlayerDBCreationForm(UserCreationForm):
class Meta: class Meta:
model = PlayerDB model = PlayerDB
username = forms.RegexField(label="Username", username = forms.RegexField(
label="Username",
max_length=30, max_length=30,
regex=r'^[\w. @+-]+$', regex=r'^[\w. @+-]+$',
widget=forms.TextInput(attrs={'size':'30'}), widget=forms.TextInput(
error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, attrs={'size': '30'}),
help_text = "30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") 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): def clean_username(self):
username = self.cleaned_data['username'] username = self.cleaned_data['username']
if PlayerDB.objects.filter(username__iexact=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 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): class PlayerForm(forms.ModelForm):
"Defines how to display Players" """
Defines how to display Players
"""
class Meta: class Meta:
model = PlayerDB model = PlayerDB
db_key = forms.RegexField(label="Username", db_key = forms.RegexField(
label="Username",
initial="PlayerDummy", initial="PlayerDummy",
max_length=30, max_length=30,
regex=r'^[\w. @+-]+$', regex=r'^[\w. @+-]+$',
required=False, required=False,
widget=forms.TextInput(attrs={'size': '30'}), widget=forms.TextInput(attrs={'size': '30'}),
error_messages = {'invalid': "This value may contain only letters, spaces, numbers and @/./+/-/_ characters."}, error_messages={
help_text = "This should be the same as the connected Player's key name. 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.") '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", db_typeclass_path = forms.CharField(
label="Typeclass",
initial=settings.BASE_PLAYER_TYPECLASS, initial=settings.BASE_PLAYER_TYPECLASS,
widget=forms.TextInput(attrs={'size':'78'}), widget=forms.TextInput(
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.") attrs={'size': '78'}),
#db_permissions = forms.CharField(label="Permissions", help_text="Required. Defines what 'type' of entity this is. This "
# initial=settings.PERMISSION_PLAYER_DEFAULT, "variable holds a Python path to a module with a valid "
# required=False, "Evennia Typeclass. Defaults to "
# widget=forms.TextInput(attrs={'size':'78'}), "settings.BASE_PLAYER_TYPECLASS.")
# 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", 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'}), widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}),
required=False, required=False,
help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form <i>type:lockfunction(args);type2:lockfunction2(args);...") help_text="In-game lock definition string. If not given, defaults "
db_cmdset_storage = forms.CharField(label="cmdset", "will be used. This string should be on the form "
"<i>type:lockfunction(args);type2:lockfunction2(args);...")
db_cmdset_storage = forms.CharField(
label="cmdset",
initial=settings.CMDSET_PLAYER, initial=settings.CMDSET_PLAYER,
widget=forms.TextInput(attrs={'size': '78'}), widget=forms.TextInput(attrs={'size': '78'}),
required=False, required=False,
help_text="python path to player cmdset class (set in settings.CMDSET_PLAYER by default)") help_text="python path to player cmdset class (set in "
"settings.CMDSET_PLAYER by default)")
class PlayerInline(admin.StackedInline): class PlayerInline(admin.StackedInline):
"Inline creation of Player" """
Inline creation of Player
"""
model = PlayerDB model = PlayerDB
template = "admin/players/stacked.html" template = "admin/players/stacked.html"
form = PlayerForm form = PlayerForm
@ -119,51 +137,80 @@ class PlayerInline(admin.StackedInline):
("In-game Permissions and Locks", ("In-game Permissions and Locks",
{'fields': ('db_lock_storage',), {'fields': ('db_lock_storage',),
#{'fields': ('db_permissions', 'db_lock_storage'), #{'fields': ('db_permissions', 'db_lock_storage'),
'description':"<i>These are permissions/locks for in-game use. They are unrelated to website access rights.</i>"}), 'description': "<i>These are permissions/locks for in-game use. "
"They are unrelated to website access rights.</i>"}),
("In-game Player data", ("In-game Player data",
{'fields': ('db_typeclass_path', 'db_cmdset_storage'), {'fields': ('db_typeclass_path', 'db_cmdset_storage'),
'description':"<i>These fields define in-game-specific properties for the Player object in-game.</i>"}), 'description': "<i>These fields define in-game-specific properties "
) "for the Player object in-game.</i>"}))
extra = 1 extra = 1
max_num = 1 max_num = 1
class PlayerTagInline(TagInline):
model = PlayerDB.db_tags.through
class PlayerAttributeInline(AttributeInline):
model = PlayerDB.db_attributes.through
class PlayerDBAdmin(BaseUserAdmin): 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') list_display = ('username', 'email', 'is_staff', 'is_superuser')
form = PlayerDBChangeForm form = PlayerDBChangeForm
add_form = PlayerDBCreationForm add_form = PlayerDBCreationForm
inlines = [PlayerTagInline, PlayerAttributeInline]
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password', 'email')}), (None, {'fields': ('username', 'password', 'email')}),
('Website profile', {'fields': ('first_name', 'last_name'), ('Website profile', {
'description': "<i>These are not used in the default system.</i>"}), 'fields': ('first_name', 'last_name'),
('Website dates', {'fields': ('last_login', 'date_joined'), 'description': "<i>These are not used "
"in the default system.</i>"}),
('Website dates', {
'fields': ('last_login', 'date_joined'),
'description': '<i>Relevant only to the website.</i>'}), 'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', ('Website Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'), 'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights.</i>"}), 'description': "<i>These are permissions/permission groups for "
('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'), "accessing the admin site. They are unrelated to "
'description': '<i>These are attributes that are more relevant to gameplay.</i>'})) "in-game access rights.</i>"}),
#('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'), ('Game Options', {
# 'description': '<i>These are attributes that are more relevant to gameplay.</i>'})) 'fields': ('db_typeclass_path', 'db_cmdset_storage',
'db_lock_storage'),
'description': '<i>These are attributes that are more relevant '
'to gameplay.</i>'}))
# ('Game Options', {'fields': (
# 'db_typeclass_path', 'db_cmdset_storage',
# 'db_permissions', 'db_lock_storage'),
# 'description': '<i>These are attributes that are '
# 'more relevant to gameplay.</i>'}))
add_fieldsets = ( add_fieldsets = (
(None, (None,
{'fields': ('username', 'password1', 'password2', 'email'), {'fields': ('username', 'password1', 'password2', 'email'),
'description':"<i>These account details are shared by the admin system and the game.</i>"},),) 'description': "<i>These account details are shared by the admin "
"system and the game.</i>"},),)
# TODO! Remove User reference! # TODO! Remove User reference!
def save_formset(self, request, form, formset, change): 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) super(PlayerDBAdmin, self).save_formset(request, form, formset, change)
userobj = form.instance userobj = form.instance
userobj.name = userobj.username userobj.name = userobj.username
if not change: if not change:
# uname, passwd, email = str(request.POST.get(u"username")), \ # uname, passwd, email = str(request.POST.get(u"username")), \
# str(request.POST.get(u"password1")), str(request.POST.get(u"email")) # str(request.POST.get(u"password1")), \
typeclass = str(request.POST.get(u"playerdb_set-0-db_typeclass_path")) # str(request.POST.get(u"email"))
typeclass = str(request.POST.get(
u"playerdb_set-0-db_typeclass_path"))
create.create_player("", "", "", create.create_player("", "", "",
user=userobj, user=userobj,
typeclass=typeclass, typeclass=typeclass,

View file

@ -2,16 +2,18 @@
# This sets up how models are displayed # This sets up how models are displayed
# in the web admin interface. # 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 src.scripts.models import ScriptDB
from django.contrib import admin from django.contrib import admin
class AttributeInline(admin.TabularInline): class ScriptTagInline(TagInline):
model = Attribute model = ScriptDB.db_tags.through
fields = ('db_key', 'db_value')
max_num = 1
class ScriptAttributeInline(AttributeInline):
model = ScriptDB.db_attributes.through
class ScriptDBAdmin(admin.ModelAdmin): class ScriptDBAdmin(admin.ModelAdmin):
@ -32,7 +34,7 @@ class ScriptDBAdmin(admin.ModelAdmin):
'db_repeats', 'db_start_delay', 'db_persistent', 'db_repeats', 'db_start_delay', 'db_persistent',
'db_obj')}), 'db_obj')}),
) )
#inlines = [AttributeInline] inlines = [ScriptTagInline, ScriptAttributeInline]
admin.site.register(ScriptDB, ScriptDBAdmin) admin.site.register(ScriptDB, ScriptDBAdmin)

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

@ -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 '<strong><a href="%s">%s</a></strong>' % (
reverse("admin:typeclasses_attribute_change",
args=[instance.attribute.id]),
instance.attribute.db_key)
key.allow_tags = True
def value(self, instance):
if not instance.id:
return "Not yet set or saved."
return instance.attribute.db_value
def strvalue(self, instance):
if not instance.id:
return "Not yet set or saved."
return instance.attribute.db_strvalue
class AttributeAdmin(ModelAdmin):
"""
Defines how to display the attributes
"""
search_fields = ('db_key', 'db_strvalue', 'db_value')
list_display = ('db_key', 'db_strvalue', 'db_value')
admin.site.register(Attribute, AttributeAdmin)
admin.site.register(Tag, TagAdmin)

View file

@ -28,15 +28,21 @@ Pickle field implementation for Django.
Modified for Evennia by Griatch. Modified for Evennia by Griatch.
""" """
from ast import literal_eval
from copy import deepcopy from copy import deepcopy
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from zlib import compress, decompress from zlib import compress, decompress
#import six # this is actually a pypy component, not in default syslib #import six # this is actually a pypy component, not in default syslib
import django import django
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
# django 1.5 introduces force_text instead of force_unicode # 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: try:
from django.utils.encoding import force_text from django.utils.encoding import force_text
except ImportError: except ImportError:
@ -120,6 +126,45 @@ def _get_subfield_superclass():
#return six.with_metaclass(models.SubfieldBase, models.Field) #return six.with_metaclass(models.SubfieldBase, models.Field)
class PickledWidget(Textarea):
def render(self, name, value, attrs=None):
value = repr(value)
try:
literal_eval(value)
except ValueError:
return value
final_attrs = self.build_attrs(attrs, name=name)
return format_html('<textarea{0}>\r\n{1}</textarea>',
flatatt(final_attrs),
force_text(value))
class PickledFormField(CharField):
widget = PickledWidget
default_error_messages = dict(CharField.default_error_messages)
default_error_messages['invalid'] = (
"This is not a Python Literal. You can store things like strings, "
"integers, or floats, but you must do it by typing them as you would "
"type them in the Python Interpreter. For instance, strings must be "
"surrounded by quote marks. We have converted it to a string for your "
"convenience. If it is acceptable, please hit save again.")
def __init__(self, *args, **kwargs):
# This needs to fall through to literal_eval.
kwargs['required'] = False
super(PickledFormField, self).__init__(*args, **kwargs)
def clean(self, value):
if value == '':
# Field was left blank. Make this None.
value = 'None'
try:
return literal_eval(value)
except ValueError:
raise ValidationError(self.error_messages['invalid'])
class PickledObjectField(_get_subfield_superclass()): class PickledObjectField(_get_subfield_superclass()):
""" """
A field that will accept *any* python object and store it in the 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): def __init__(self, *args, **kwargs):
self.compress = kwargs.pop('compress', False) self.compress = kwargs.pop('compress', False)
self.protocol = kwargs.pop('protocol', DEFAULT_PROTOCOL) self.protocol = kwargs.pop('protocol', DEFAULT_PROTOCOL)
kwargs.setdefault('editable', False)
super(PickledObjectField, self).__init__(*args, **kwargs) super(PickledObjectField, self).__init__(*args, **kwargs)
def get_default(self): def get_default(self):
@ -180,6 +224,9 @@ class PickledObjectField(_get_subfield_superclass()):
return value._obj return value._obj
return value return value
def formfield(self, **kwargs):
return PickledFormField(**kwargs)
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
value = super(PickledObjectField, self).pre_save(model_instance, add) value = super(PickledObjectField, self).pre_save(model_instance, add)
return wrap_conflictual_object(value) return wrap_conflictual_object(value)