From b80f7c36376e6f31334c92d80c28284775af5ee4 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 8 Nov 2016 04:16:10 -0500 Subject: [PATCH] Attributes in django admin now also editable inline, causing them to call the AttributeHandler or NicksHandler of the appropriate object. --- evennia/comms/admin.py | 2 + evennia/objects/admin.py | 1 + evennia/players/admin.py | 1 + evennia/scripts/admin.py | 1 + evennia/typeclasses/admin.py | 280 +++++++++++++++++++++++++++-------- 5 files changed, 221 insertions(+), 64 deletions(-) diff --git a/evennia/comms/admin.py b/evennia/comms/admin.py index 38086e3dd0..b1e54d706d 100644 --- a/evennia/comms/admin.py +++ b/evennia/comms/admin.py @@ -14,6 +14,7 @@ class ChannelAttributeInline(AttributeInline): """ model = ChannelDB.db_attributes.through + related_field = "channeldb" class ChannelTagInline(TagInline): @@ -22,6 +23,7 @@ class ChannelTagInline(TagInline): """ model = ChannelDB.db_tags.through + related_field = "channeldb" class MsgAdmin(admin.ModelAdmin): diff --git a/evennia/objects/admin.py b/evennia/objects/admin.py index 7ad78a2f2a..c20e6c1ed4 100644 --- a/evennia/objects/admin.py +++ b/evennia/objects/admin.py @@ -18,6 +18,7 @@ class ObjectAttributeInline(AttributeInline): """ model = ObjectDB.db_attributes.through + related_field = "objectdb" class ObjectTagInline(TagInline): diff --git a/evennia/players/admin.py b/evennia/players/admin.py index 94ccabd547..ecc61311d8 100644 --- a/evennia/players/admin.py +++ b/evennia/players/admin.py @@ -182,6 +182,7 @@ class PlayerAttributeInline(AttributeInline): """ model = PlayerDB.db_attributes.through + related_field = "playerdb" class PlayerDBAdmin(BaseUserAdmin): diff --git a/evennia/scripts/admin.py b/evennia/scripts/admin.py index b3084da58e..ce8983d20e 100644 --- a/evennia/scripts/admin.py +++ b/evennia/scripts/admin.py @@ -23,6 +23,7 @@ class ScriptAttributeInline(AttributeInline): """ model = ScriptDB.db_attributes.through + related_field = "scriptdb" class ScriptDBAdmin(admin.ModelAdmin): diff --git a/evennia/typeclasses/admin.py b/evennia/typeclasses/admin.py index 74c84bf80e..262493dc0b 100644 --- a/evennia/typeclasses/admin.py +++ b/evennia/typeclasses/admin.py @@ -1,14 +1,14 @@ from django.contrib import admin from django.contrib.admin import ModelAdmin -from django.core.urlresolvers import reverse from evennia.typeclasses.models import Attribute, Tag from django import forms +from evennia.utils.picklefield import PickledFormField +import traceback class TagAdmin(admin.ModelAdmin): """ A django Admin wrapper for Tags. - """ search_fields = ('db_key', 'db_category', 'db_tagtype') list_display = ('db_key', 'db_category', 'db_tagtype', 'db_data') @@ -17,22 +17,54 @@ class TagAdmin(admin.ModelAdmin): class TagForm(forms.ModelForm): + """ + This form overrides the base behavior of the ModelForm that would be used for a Tag-through-model. + Since the through-models only have access to the foreignkeys of the Tag and the Object that they're + attached to, we need to spoof the behavior of it being a form that would correspond to its tag, + or the creation of a tag. Instead of being saved, we'll call to the Object's handler, which will handle + the creation, change, or deletion of a tag for us, as well as updating the handler's cache so that all + changes are instantly updated in-game. + """ tag_key = forms.CharField(label='Tag Name') tag_category = forms.CharField(label="Category", required=False) tag_type = forms.CharField(label="Type", required=False) tag_data = forms.CharField(label="Data", required=False) def __init__(self, *args, **kwargs): + """ + If we have a tag, then we'll prepopulate our instance with the fields we'd expect it + to have based on the tag. tag_key, tag_category, tag_type, and tag_data all refer to + the corresponding tag fields. The initial data of the form fields will similarly be + populated. + """ super(TagForm, self).__init__(*args, **kwargs) + tagkey = None + tagcategory = None + tagtype = None + tagdata = None if hasattr(self.instance, 'tag'): - self.fields['tag_key'].initial = self.instance.tag.db_key - self.fields['tag_category'].initial = self.instance.tag.db_category - self.fields['tag_type'].initial = self.instance.tag.db_tagtype - self.fields['tag_data'].initial = self.instance.tag.db_data + tagkey = self.instance.tag.db_key + tagcategory = self.instance.tag.db_category + tagtype = self.instance.tag.db_tagtype + tagdata = self.instance.tag.db_data + self.fields['tag_key'].initial = tagkey + self.fields['tag_category'].initial = tagcategory + self.fields['tag_type'].initial = tagtype + self.fields['tag_data'].initial = tagdata + self.instance.tag_key = tagkey + self.instance.tag_category = tagcategory + self.instance.tag_type = tagtype + self.instance.tag_data = tagdata def save(self, commit=True): + """ + One thing we want to do here is the or None checks, because forms are saved with an empty + string rather than null from forms, usually, and the Handlers may handle empty strings + differently than None objects. So for consistency with how things are handled in game, + we'll try to make sure that empty form fields will be None, rather than ''. + """ # we are spoofing a tag for the Handler that will be called - #instance = super(TagForm, self).save(commit=False) + # instance = super(TagForm, self).save(commit=False) instance = self.instance instance.tag_key = self.cleaned_data['tag_key'] instance.tag_category = self.cleaned_data['tag_category'] or None @@ -42,8 +74,16 @@ class TagForm(forms.ModelForm): class TagFormSet(forms.BaseInlineFormSet): + """ + The Formset handles all the inline forms that are grouped together on the change page of the + corresponding object. All the tags will appear here, and we'll save them by overriding the + formset's save method. The forms will similarly spoof their save methods to return an instance + which hasn't been saved to the database, but have the relevant fields filled out based on the + contents of the cleaned form. We'll then use that to call to the handler of the corresponding + Object, where the handler is an AliasHandler, PermissionsHandler, or TagHandler, based on the + type of tag. + """ def save(self, commit=True): - print "inside TagFormSet" def get_handler(finished_object): related = getattr(finished_object, self.related_field) try: @@ -58,32 +98,30 @@ class TagFormSet(forms.BaseInlineFormSet): handler_name = "tags" return getattr(related, handler_name) instances = super(TagFormSet, self).save(commit=False) + # self.deleted_objects is a list created when super of save is called, we'll remove those for obj in self.deleted_objects: handler = get_handler(obj) - try: - tagkey = obj.tag_key - except AttributeError: - tagkey = obj.tag.db_key - handler.remove(tagkey) + handler.remove(obj.tag_key, category=obj.tag_category) for instance in instances: handler = get_handler(instance) - handler.add(instance.tag_key) + handler.add(instance.tag_key, category=instance.tag_category, data=instance.tag_data) class TagInline(admin.TabularInline): """ - A handler for inline Tags. - + A handler for inline Tags. This class should be subclassed in the admin of your models, + and the 'model' and 'related_field' class attributes must be set. model should be the + through model (ObjectDB_db_tag', for example), while related field should be the name + of the field on that through model which points to the model being used: 'objectdb', + 'msg', 'playerdb', etc. """ # Set this to the through model of your desired M2M when subclassing. model = None form = TagForm formset = TagFormSet - related_field = None - #fields = ('tag', 'key', 'category', 'data', 'tagtype') + related_field = None # Must be 'objectdb', 'playerdb', 'msg', etc. Set when subclassing raw_id_fields = ('tag',) readonly_fields = ('tag',) - #readonly_fields = ('key', 'category', 'data', 'tagtype') extra = 0 def get_formset(self, request, obj=None, **kwargs): @@ -94,68 +132,182 @@ class TagInline(admin.TabularInline): people used the admin at the same time """ formset = super(TagInline, self).get_formset(request, obj, **kwargs) + class ProxyFormset(formset): pass ProxyFormset.related_field = self.related_field return ProxyFormset - def key(self, instance): - if not instance.id: - return "Not yet set or saved." - return '%s' % ( - reverse("admin:typeclasses_tag_change", - args=[instance.tag.id]), - instance.tag.db_key) - key.allow_tags = True - - def category(self, instance): - if not instance.id: - return "Not yet set or saved." - return instance.tag.db_category - def data(self, instance): - if not instance.id: - return "Not yet set or saved." - return instance.tag.db_data - - def tagtype(self, instance): - if not instance.id: - return "Not yet set or saved." - return instance.tag.db_tagtype +class AttributeForm(forms.ModelForm): + """ + This form overrides the base behavior of the ModelForm that would be used for a Tag-through-model. + Since the through-models only have access to the foreignkeys of the Tag and the Object that they're + attached to, we need to spoof the behavior of it being a form that would correspond to its tag, + or the creation of a tag. Instead of being saved, we'll call to the Object's handler, which will handle + the creation, change, or deletion of a tag for us, as well as updating the handler's cache so that all + changes are instantly updated in-game. + """ + attr_key = forms.CharField(label='Attribute Name') + attr_category = forms.CharField(label="Category", help_text="type of attribute, for sorting", required=False) + attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False) + attr_type = forms.CharField(label="Type", help_text="nick for nickname, else leave blank", required=False) + attr_strvalue = forms.CharField(label="String Value", + help_text="Only enter this if value is blank and you want to save as a string", + required=False) + attr_lockstring = forms.CharField(label="Locks", required=False, widget=forms.Textarea) + + def __init__(self, *args, **kwargs): + """ + If we have a tag, then we'll prepopulate our instance with the fields we'd expect it + to have based on the tag. tag_key, tag_category, tag_type, and tag_data all refer to + the corresponding tag fields. The initial data of the form fields will similarly be + populated. + """ + super(AttributeForm, self).__init__(*args, **kwargs) + attr_key = None + attr_category = None + attr_value = None + attr_strvalue = None + attr_type = None + attr_lockstring = None + if hasattr(self.instance, 'attribute'): + attr_key = self.instance.attribute.db_key + attr_category = self.instance.attribute.db_category + attr_value = self.instance.attribute.db_value + attr_strvalue = self.instance.attribute.db_strvalue + attr_type = self.instance.attribute.db_attrtype + attr_lockstring = self.instance.attribute.db_lock_storage + self.fields['attr_key'].initial = attr_key + self.fields['attr_category'].initial = attr_category + self.fields['attr_type'].initial = attr_type + self.fields['attr_value'].initial = attr_value + self.fields['attr_strvalue'].initial = attr_strvalue + self.fields['attr_lockstring'].initial = attr_lockstring + self.instance.attr_key = attr_key + self.instance.attr_category = attr_category + self.instance.attr_value = attr_value + self.instance.attr_strvalue = attr_strvalue + self.instance.attr_type = attr_type + self.instance.attr_lockstring = attr_lockstring + + def save(self, commit=True): + """ + One thing we want to do here is the or None checks, because forms are saved with an empty + string rather than null from forms, usually, and the Handlers may handle empty strings + differently than None objects. So for consistency with how things are handled in game, + we'll try to make sure that empty form fields will be None, rather than ''. + """ + # we are spoofing a tag for the Handler that will be called + # instance = super(TagForm, self).save(commit=False) + instance = self.instance + instance.attr_key = self.cleaned_data['attr_key'] + instance.attr_category = self.cleaned_data['attr_category'] or None + instance.attr_value = self.cleaned_data['attr_value'] or None + instance.attr_strvalue = self.cleaned_data['attr_strvalue'] or None + instance.attr_type = self.cleaned_data['attr_type'] or None + instance.attr_lockstring = self.cleaned_data['attr_lockstring'] + return instance + + +class AttributeFormSet(forms.BaseInlineFormSet): + """ + Attribute version of TagFormSet, as above. + """ + def save(self, commit=True): + def get_handler(finished_object): + related = getattr(finished_object, self.related_field) + try: + attrtype = finished_object.attr_type + except AttributeError: + attrtype = finished_object.attribute.db_attrtype + if attrtype == "nick": + handler_name = "nicks" + else: + handler_name = "attributes" + return getattr(related, handler_name) + instances = super(AttributeFormSet, self).save(commit=False) + # self.deleted_objects is a list created when super of save is called, we'll remove those + for obj in self.deleted_objects: + handler = get_handler(obj) + handler.remove(obj.attr_key, category=obj.attr_category) + for instance in instances: + handler = get_handler(instance) + strattr = True if instance.attr_strvalue else False + value = instance.attr_value or instance.attr_strvalue + try: + handler.add(instance.attr_key, value, category=instance.attr_category, strattr=strattr, + lockstring=instance.attr_lockstring) + except (TypeError, ValueError): + # catch errors in nick templates and continue + traceback.print_exc() + continue class AttributeInline(admin.TabularInline): """ - Inline creation of player attributes.j - + A handler for inline Tags. This class should be subclassed in the admin of your models, + and the 'model' and 'related_field' class attributes must be set. model should be the + through model (ObjectDB_db_tag', for example), while related field should be the name + of the field on that through model which points to the model being used: 'objectdb', + 'msg', 'playerdb', etc. """ # Set this to the through model of your desired M2M when subclassing. model = None - extra = 1 - #form = AttributeForm - fields = ('attribute', 'key', 'value', 'strvalue') + form = AttributeForm + formset = AttributeFormSet + related_field = None # Must be 'objectdb', 'playerdb', 'msg', etc. Set when subclassing raw_id_fields = ('attribute',) - readonly_fields = ('key', 'value', 'strvalue') + readonly_fields = ('attribute',) + extra = 0 - 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) + def get_formset(self, request, obj=None, **kwargs): + """ + get_formset has to return a class, but we need to make the class that we return + know about the related_field that we'll use. Returning the class itself rather than + a proxy isn't threadsafe, since it'd be the base class and would change if multiple + people used the admin at the same time + """ + formset = super(AttributeInline, self).get_formset(request, obj, **kwargs) - key.allow_tags = True + class ProxyFormset(formset): + pass - def value(self, instance): - if not instance.id: - return "Not yet set or saved." - return instance.attribute.value + ProxyFormset.related_field = self.related_field + return ProxyFormset - def strvalue(self, instance): - if not instance.id: - return "Not yet set or saved." - return instance.attribute.strvalue +# class AttributeInline(admin.TabularInline): +# """ +# Inline creation of player attributes.j +# +# """ +# # 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.value +# +# def strvalue(self, instance): +# if not instance.id: +# return "Not yet set or saved." +# return instance.attribute.strvalue class AttributeAdmin(ModelAdmin): @@ -163,7 +315,7 @@ class AttributeAdmin(ModelAdmin): Defines how to display the attributes. """ - search_fields = ('db_key', 'db_strvalue', 'db_value') + search_fields = ('db_key', 'db_category') list_display = ('db_key', 'db_strvalue', 'db_value') permitted_types = ('str', 'unicode', 'int', 'float', 'NoneType', 'bool')