diff --git a/evennia/comms/models.py b/evennia/comms/models.py index 40a05c7d91..fbb8bd92de 100644 --- a/evennia/comms/models.py +++ b/evennia/comms/models.py @@ -78,7 +78,7 @@ class Msg(SharedMemoryModel): "accounts.AccountDB", related_name="sender_account_set", blank=True, - verbose_name="sender(account)", + verbose_name="Senders (Accounts)", db_index=True, ) @@ -86,14 +86,14 @@ class Msg(SharedMemoryModel): "objects.ObjectDB", related_name="sender_object_set", blank=True, - verbose_name="sender(object)", + verbose_name="Senders (Objects)", db_index=True, ) db_sender_scripts = models.ManyToManyField( "scripts.ScriptDB", related_name="sender_script_set", blank=True, - verbose_name="sender(script)", + verbose_name="Senders (Scripts)", db_index=True, ) db_sender_external = models.CharField( @@ -102,14 +102,15 @@ class Msg(SharedMemoryModel): null=True, blank=True, db_index=True, - help_text="identifier for external sender, for example a sender over an " - "IRC connection (i.e. someone who doesn't have an exixtence in-game).", + help_text="Identifier for single external sender, for use with senders " + "not represented by a regular database model." ) db_receivers_accounts = models.ManyToManyField( "accounts.AccountDB", related_name="receiver_account_set", blank=True, + verbose_name="Receivers (Accounts)", help_text="account receivers", ) @@ -117,12 +118,14 @@ class Msg(SharedMemoryModel): "objects.ObjectDB", related_name="receiver_object_set", blank=True, + verbose_name="Receivers (Objects)", help_text="object receivers", ) db_receivers_scripts = models.ManyToManyField( "scripts.ScriptDB", related_name="receiver_script_set", blank=True, + verbose_name="Receivers (Scripts)", help_text="script_receivers", ) @@ -132,8 +135,8 @@ class Msg(SharedMemoryModel): null=True, blank=True, db_index=True, - help_text="identifier for single external receiver, for use with " - "receivers without a database existence." + help_text="Identifier for single external receiver, for use with recievers " + "not represented by a regular database model." ) # header could be used for meta-info about the message if your system needs diff --git a/evennia/settings_default.py b/evennia/settings_default.py index e83b0e9fa3..c9a71904dc 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -850,8 +850,12 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = False + # Where to find locales (no need to change this, most likely) LOCALE_PATHS = [os.path.join(EVENNIA_DIR, "locale/")] +# How to display time stamps in e.g. the admin +SHORT_DATETIME_FORMAT = 'Y-m-d H:i:s.u' +DATETIME_FORMAT = 'Y-m-d H:i:s' # ISO 8601 but without T and timezone # This should be turned off unless you want to do tests with Django's # development webserver (normally Evennia runs its own server) SERVE_MEDIA = False diff --git a/evennia/web/admin/accounts.py b/evennia/web/admin/accounts.py index 2c5980d700..00f3f1beb7 100644 --- a/evennia/web/admin/accounts.py +++ b/evennia/web/admin/accounts.py @@ -22,6 +22,7 @@ from evennia.accounts.models import AccountDB from evennia.utils import create from .attributes import AttributeInline from .tags import TagInline +from . import utils as adminutils sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) @@ -49,6 +50,31 @@ class AccountChangeForm(UserChangeForm): help_text="30 characters or fewer. Letters, spaces, digits and " "@/./+/-/_ only.", ) + db_typeclass_path = forms.ChoiceField( + label="Typeclass", + help_text="This is the Python-path to the class implementing the actual account functionality. " + "You usually don't need to change this from the default.
" + "If your custom class is not found here, it may not be imported as part of Evennia's startup.", + choices=adminutils.get_and_load_typeclasses(parent=AccountDB), + ) + + db_lock_storage = forms.CharField( + label="Locks", + required=False, + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + help_text="Locks limit access to the entity. Written on form `type:lockdef;type:lockdef..." + "
(Permissions (used with the perm() lockfunc) are Tags with the 'permission' type)", + ) + + db_cmdset_storage = forms.CharField( + label="CommandSet", + initial=settings.CMDSET_ACCOUNT, + widget=forms.TextInput(attrs={"size": "78"}), + required=False, + help_text="Python path to account cmdset class (set via " + "settings.CMDSET_ACCOUNT by default)", + ) + def clean_username(self): """ Clean the username and check its existence. @@ -99,7 +125,7 @@ class AccountForm(forms.ModelForm): """ - class Meta(object): + class Meta: model = AccountDB fields = "__all__" app_label = "accounts" @@ -120,27 +146,21 @@ class AccountForm(forms.ModelForm): "@/./+/-/_ only.", ) - db_typeclass_path = forms.CharField( + db_typeclass_path = forms.ChoiceField( label="Typeclass", - initial=settings.BASE_ACCOUNT_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_ACCOUNT_TYPECLASS.", + initial={settings.BASE_ACCOUNT_TYPECLASS: settings.BASE_ACCOUNT_TYPECLASS}, + help_text="This is the Python-path to the class implementing the actual " + "account functionality. You usually don't need to change this from" + "the default.
If your custom class is not found here, it may not be " + "imported as part of Evennia's startup.", + choices=adminutils.get_and_load_typeclasses(parent=AccountDB), ) - db_permissions = forms.CharField( - label="Permissions", - initial=settings.PERMISSION_ACCOUNT_DEFAULT, + db_lock_storage = forms.CharField( + label="Locks", 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 an Account have permission " - "'Admin', 'Builder' etc. An Account permission can be " - "overloaded by the permissions of a controlled Character. " - "Normal accounts use 'Accounts' by default.", + help_text="Locks limit access to the entity. Written on form `type:lockdef;type:lockdef..." + "
(Permissions (used with the perm() lockfunc) are Tags with the 'permission' type)", ) db_lock_storage = forms.CharField( @@ -220,48 +240,45 @@ class AccountAdmin(BaseUserAdmin): This is the main creation screen for Users/accounts """ + list_display = ("username", "email", "is_staff", "is_superuser") form = AccountChangeForm add_form = AccountCreationForm inlines = [AccountTagInline, AccountAttributeInline] + readonly_fields = ["db_date_created", "serialized_string"] fieldsets = ( - (None, {"fields": ("username", "password", "email")}), ( - "Website profile", + None, { - "fields": ("first_name", "last_name"), - "description": "These are not used " "in the default system.", + "fields": ( + ("username", "db_typeclass_path"), + "password", + "email", + "db_date_created", + "db_lock_storage", + "db_cmdset_storage", + "serialized_string", + ) }, ), ( - "Website dates", + "Admin/Website properties", { - "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.", + "fields": ( + ("first_name", "last_name"), + "last_login", + "date_joined", + "is_active", + "is_staff", + "is_superuser", + "user_permissions", + "groups", + ), + "description": "Used by the website/Django admin. " + "Except for `superuser status`, the permissions are not used in-game.", }, ), ) - # ('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 = ( ( @@ -274,6 +291,31 @@ class AccountAdmin(BaseUserAdmin): ), ) + def serialized_string(self, obj): + """ + Get the serialized version of the object. + + """ + from evennia.utils import dbserialize + + return str(dbserialize.pack_dbobj(obj)) + + serialized_string.help_text = ( + "Copy & paste this string into an Attribute's `value` field to store it there. " + "Note that you cannot (easily) add multiple accounts this way - better do that " + "in code." + ) + + def get_form(self, request, obj=None, **kwargs): + """ + Overrides help texts. + + """ + help_texts = kwargs.get("help_texts", {}) + help_texts["serialized_string"] = self.serialized_string.help_text + kwargs["help_texts"] = help_texts + return super().get_form(request, obj, **kwargs) + @sensitive_post_parameters_m def user_change_password(self, request, id, form_url=""): user = self.get_object(request, unquote(id)) diff --git a/evennia/web/admin/attributes.py b/evennia/web/admin/attributes.py index 74617babc3..41d31286d9 100644 --- a/evennia/web/admin/attributes.py +++ b/evennia/web/admin/attributes.py @@ -27,17 +27,28 @@ class AttributeForm(forms.ModelForm): """ attr_key = forms.CharField( - label="Attribute Name", required=False, initial="Enter Attribute Name Here" + label="Attribute Name", required=False, initial="Enter Attribute Name Here", + help_text="The main identifier of the Attribute. For Nicks, this is the pattern-matching string." ) attr_category = forms.CharField( - label="Category", help_text="type of attribute, for sorting", required=False, max_length=128 + label="Category", + help_text="Categorization. Unset (default) gives a category of `None`, which is " + "is what is searched with e.g. `obj.db.attrname`. For 'nick'-type attributes, this is usually " + "'inputline' or 'channel'.", + required=False, max_length=128 ) - attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False) - attr_type = forms.CharField( + attr_value = PickledFormField( + label="Value", + help_text="Value to pickle/save. Db-objects are serialized as a list " + "containing `__packed_dbobj__` (they can't easily be added from here). Nicks " + "store their pattern-replacement here.", + required=False + ) + attr_type = forms.ChoiceField( label="Type", - help_text='Internal use. Either unset (normal Attribute) or "nick"', - required=False, - max_length=16, + choices=[(None, "-"), ("nick", "nick")], + help_text="Unset for regular Attributes, 'nick' for Nick-replacement usage.", + required=False ) attr_lockstring = forms.CharField( label="Locks", @@ -172,6 +183,7 @@ class AttributeInline(admin.TabularInline): # Set this to the through model of your desired M2M when subclassing. model = None verbose_name = "Attribute" + verbose_name_plural = "Attributes" form = AttributeForm formset = AttributeFormSet related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing diff --git a/evennia/web/admin/comms.py b/evennia/web/admin/comms.py index fb87b0130e..9c2c8e959a 100644 --- a/evennia/web/admin/comms.py +++ b/evennia/web/admin/comms.py @@ -3,14 +3,118 @@ This defines how Comm models are displayed in the web admin interface. """ +from django import forms from django.contrib import admin -from evennia.comms.models import ChannelDB +from evennia.comms.models import ChannelDB, Msg from django.conf import settings from .attributes import AttributeInline from .tags import TagInline +class MsgTagInline(TagInline): + """ + Inline display for Msg-tags. + + """ + model = Msg.db_tags.through + related_field = "msg" + +class MsgForm(forms.ModelForm): + """ + Custom Msg form. + + """ + + class Meta: + models = Msg + fields = "__all__" + + db_header = forms.CharField( + label="Header", + required=False, + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + help_text="Optional header for the message; it could be a title or " + "metadata depending on msg-use." + ) + + db_lock_storage = forms.CharField( + label="Locks", + required=False, + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + 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);...", + ) + + + +@admin.register(Msg) +class MsgAdmin(admin.ModelAdmin): + """ + Defines display for Msg objects + + """ + + list_display = ( + "id", + "db_date_created", + "sender", + "receiver", + "start_of_message" + ) + list_display_links = ("id", "db_date_created", "start_of_message") + inlines = [MsgTagInline] + form = MsgForm + ordering = ["db_date_created", ] + # readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels'] + search_fields = ["id", "^db_date_created", "^db_message"] + readonly_fields = ["db_date_created"] + save_as = True + save_on_top = True + list_select_related = True + raw_id_fields = ( + "db_date_created", "db_sender_accounts", + "db_sender_objects", "db_sender_scripts", + "db_receivers_accounts", "db_receivers_objects", + "db_receivers_scripts", "db_hide_from_accounts", + "db_hide_from_objects") + + fieldsets = ( + ( + None, + { + "fields": ( + ("db_sender_accounts", "db_sender_objects", "db_sender_scripts", "db_sender_external"), + ("db_receivers_accounts", "db_receivers_objects", "db_receivers_scripts", "db_receiver_external"), + ("db_hide_from_accounts", "db_hide_from_objects"), + "db_header", + "db_message" + ) + }, + ), + ) + + def sender(self, obj): + senders = [o for o in obj.senders if o] + if senders: + return senders[0] + sender.help_text = "If multiple, only the first is shown." + + def receiver(self, obj): + receivers = [o for o in obj.receivers if o] + if receivers: + return receivers[0] + receiver.help_text = "If multiple, only the first is shown." + + def start_of_message(self, obj): + crop_length = 50 + if obj.db_message: + msg = obj.db_message + if len(msg) > (crop_length - 5): + msg = msg[:50] + "[...]" + return msg + class ChannelAttributeInline(AttributeInline): """ Inline display of Channel Attribute - experimental @@ -31,33 +135,26 @@ class ChannelTagInline(TagInline): related_field = "channeldb" -class MsgAdmin(admin.ModelAdmin): +class ChannelForm(forms.ModelForm): """ - Defines display for Msg objects + Form for accessing channels. """ + class Meta: + model = ChannelDB + fields = "__all__" - list_display = ( - "id", - "db_date_created", - "db_sender", - "db_receivers", - "db_channels", - "db_message", - "db_lock_storage", + db_lock_storage = forms.CharField( + label="Locks", + required=False, + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + 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);...", ) - list_display_links = ("id",) - ordering = ["db_date_created", "db_sender", "db_receivers", "db_channels"] - # readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels'] - search_fields = ["id", "^db_date_created", "^db_message"] - save_as = True - save_on_top = True - list_select_related = True - - -# admin.site.register(Msg, MsgAdmin) +@admin.register(ChannelDB) class ChannelAdmin(admin.ModelAdmin): """ Defines display for Channel objects @@ -65,6 +162,7 @@ class ChannelAdmin(admin.ModelAdmin): """ inlines = [ChannelTagInline, ChannelAttributeInline] + form = ChannelForm list_display = ("id", "db_key", "no_of_subscribers", "db_lock_storage") list_display_links = ("id", "db_key") ordering = ["db_key"] @@ -130,6 +228,3 @@ class ChannelAdmin(admin.ModelAdmin): from django.urls import reverse return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id])) - - -admin.site.register(ChannelDB, ChannelAdmin) diff --git a/evennia/web/admin/help.py b/evennia/web/admin/help.py index 2bed18c69e..272772c5fe 100644 --- a/evennia/web/admin/help.py +++ b/evennia/web/admin/help.py @@ -27,9 +27,10 @@ class HelpEntryForm(forms.ModelForm): label="Locks", initial="view:all()", required=False, - widget=forms.TextInput(attrs={"size": "40"}), - ) - + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + help_text="Set lock to view:all() unless you want it to only show to certain users." + "
Use the `edit:` limit if wanting to limit who can edit from in-game. By default it's only " + "limited to who can use the `sethelp` command (Builders).") class HelpEntryAdmin(admin.ModelAdmin): "Sets up the admin manaager for help entries" @@ -48,7 +49,6 @@ class HelpEntryAdmin(admin.ModelAdmin): None, { "fields": (("db_key", "db_help_category"), "db_entrytext", "db_lock_storage"), - "description": "Sets a Help entry. Set lock to view:all() unless you want to restrict it.", }, ), ) diff --git a/evennia/web/admin/objects.py b/evennia/web/admin/objects.py index 2429e4f2d0..118a0a51e9 100644 --- a/evennia/web/admin/objects.py +++ b/evennia/web/admin/objects.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext as _ from evennia.objects.models import ObjectDB from .attributes import AttributeInline from .tags import TagInline +from . import utils as adminutils class ObjectAttributeInline(AttributeInline): @@ -49,15 +50,15 @@ class ObjectCreateForm(forms.ModelForm): help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. " "If creating a Character, check so the name is unique among characters!", ) - db_typeclass_path = forms.CharField( + db_typeclass_path = forms.ChoiceField( label="Typeclass", - initial=settings.BASE_OBJECT_TYPECLASS, - widget=forms.TextInput(attrs={"size": "78"}), - help_text="This defines what 'type' of entity this is. This variable holds a " - "Python path to a module with a valid Evennia Typeclass. If you are " - "creating a Character you should use the typeclass defined by " - "settings.BASE_CHARACTER_TYPECLASS or one derived from that.", - ) + initial={settings.BASE_OBJECT_TYPECLASS: settings.BASE_OBJECT_TYPECLASS}, + help_text="This is the Python-path to the class implementing the actual functionality. " + f"
If you are creating a Character you usually need {settings.BASE_CHARACTER_TYPECLASS} " + "or a subclass of that.
If your custom class is not found in the list, it may not be imported " + "as part of Evennia's startup.", + choices=adminutils.get_and_load_typeclasses(parent=ObjectDB)) + db_cmdset_storage = forms.CharField( label="CmdSet", initial="", @@ -66,6 +67,7 @@ class ObjectCreateForm(forms.ModelForm): help_text="Most non-character objects don't need a cmdset" " and can leave this field blank.", ) + raw_id_fields = ("db_destination", "db_location", "db_home") @@ -75,11 +77,11 @@ class ObjectEditForm(ObjectCreateForm): """ - class Meta(object): + class Meta: + model = ObjectDB fields = "__all__" - db_lock_storage = forms.CharField( - label="Locks", + db_lock_storage = forms.CharField( label="Locks", required=False, widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), help_text="In-game lock definition string. If not given, defaults will be used. " @@ -87,6 +89,12 @@ class ObjectEditForm(ObjectCreateForm): "type:lockfunction(args);type2:lockfunction2(args);...", ) + db_typeclass_path = forms.ChoiceField( + label="Typeclass", + help_text="This is the Python-path to the class implementing the actual object functionality. " + "
If your custom class is not found here, it may not be imported as part of Evennia's startup.", + choices=adminutils.get_and_load_typeclasses(parent=ObjectDB)) + @admin.register(ObjectDB) class ObjectAdmin(admin.ModelAdmin): @@ -101,6 +109,7 @@ class ObjectAdmin(admin.ModelAdmin): ordering = ["db_account", "db_typeclass_path", "id"] search_fields = ["=id", "^db_key", "db_typeclass_path", "^db_account__db_key"] raw_id_fields = ("db_destination", "db_location", "db_home") + readonly_fields = ("serialized_string", ) save_as = True save_on_top = True @@ -116,10 +125,10 @@ class ObjectAdmin(admin.ModelAdmin): { "fields": ( ("db_key", "db_typeclass_path"), - ("db_lock_storage",), - ("db_location", "db_home"), - "db_destination", + ("db_location", "db_home", "db_destination"), "db_cmdset_storage", + "db_lock_storage", + "serialized_string" ) }, ), @@ -140,6 +149,19 @@ class ObjectAdmin(admin.ModelAdmin): ), ) + def serialized_string(self, obj): + """ + Get the serialized version of the object. + + """ + from evennia.utils import dbserialize + return str(dbserialize.pack_dbobj(obj)) + + serialized_string.help_text = ( + "Copy & paste this string into an Attribute's `value` field to store it there. " + "Note that you cannot (easily) add multiple objects this way - better do that " + "in code.") + def get_fieldsets(self, request, obj=None): """ Return fieldsets. @@ -161,12 +183,16 @@ class ObjectAdmin(admin.ModelAdmin): obj (Object, optional): Database object. """ + help_texts = kwargs.get("help_texts", {}) + help_texts["serialized_string"] = self.serialized_string.help_text + kwargs["help_texts"] = help_texts + defaults = {} if obj is None: defaults.update( {"form": self.add_form, "fields": flatten_fieldsets(self.add_fieldsets)} ) - defaults.update(kwargs) + defaults.update(kwargs) return super().get_form(request, obj, **defaults) def save_model(self, request, obj, form, change): diff --git a/evennia/web/admin/scripts.py b/evennia/web/admin/scripts.py index d0cddff39b..f8dab474b6 100644 --- a/evennia/web/admin/scripts.py +++ b/evennia/web/admin/scripts.py @@ -2,12 +2,55 @@ # This sets up how models are displayed # in the web admin interface. # +from django import forms from django.conf import settings from django.contrib import admin from evennia.scripts.models import ScriptDB from .attributes import AttributeInline from .tags import TagInline +from . import utils as adminutils + + +class ScriptForm(forms.ModelForm): + + db_key = forms.CharField( + label = "Name/Key", + help_text="Script identifier, shown in listings etc." + ) + + db_typeclass_path = forms.ChoiceField( + label="Typeclass", + help_text="This is the Python-path to the class implementing the actual script functionality. " + "
If your custom class is not found here, it may not be imported as part of Evennia's startup.", + choices=adminutils.get_and_load_typeclasses( + parent=ScriptDB, excluded_parents=["evennia.prototypes.prototypes.DbPrototype"]) + ) + + db_lock_storage = forms.CharField( label="Locks", + required=False, + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + 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_interval = forms.IntegerField( + label="Repeat Interval", + help_text="Optional timer component.
How often to call the Script's
`at_repeat` hook, in seconds." + "
Set to 0 to disable." + ) + db_repeats = forms.IntegerField( + help_text="Only repeat this many times." + "
Set to 0 to run indefinitely." + ) + db_start_delay = forms.BooleanField( + help_text="Wait Interval seconds before first call." + ) + db_persistent = forms.BooleanField( + label = "Survives reboot", + help_text="If unset, a server reboot will remove the timer." + ) class ScriptTagInline(TagInline): @@ -17,6 +60,7 @@ class ScriptTagInline(TagInline): """ model = ScriptDB.db_tags.through + form = ScriptForm related_field = "scriptdb" @@ -27,6 +71,7 @@ class ScriptAttributeInline(AttributeInline): """ model = ScriptDB.db_attributes.through + form = ScriptForm related_field = "scriptdb" @@ -49,6 +94,8 @@ class ScriptAdmin(admin.ModelAdmin): list_display_links = ("id", "db_key") ordering = ["db_obj", "db_typeclass_path"] search_fields = ["^db_key", "db_typeclass_path"] + readonly_fields = ["serialized_string"] + form = ScriptForm save_as = True save_on_top = True list_select_related = True @@ -60,17 +107,40 @@ class ScriptAdmin(admin.ModelAdmin): { "fields": ( ("db_key", "db_typeclass_path"), - "db_interval", - "db_repeats", - "db_start_delay", - "db_persistent", + ("db_interval", "db_repeats", "db_start_delay", "db_persistent"), "db_obj", + "db_lock_storage", + "serialized_string" ) }, ), ) inlines = [ScriptTagInline, ScriptAttributeInline] + def serialized_string(self, obj): + """ + Get the serialized version of the object. + + """ + from evennia.utils import dbserialize + return str(dbserialize.pack_dbobj(obj)) + + serialized_string.help_text = ( + "Copy & paste this string into an Attribute's `value` field to store it there. " + "Note that you cannot (easily) add multiple scripts this way - better do that " + "in code.") + + + def get_form(self, request, obj=None, **kwargs): + """ + Overrides help texts. + + """ + help_texts = kwargs.get("help_texts", {}) + help_texts["serialized_string"] = self.serialized_string.help_text + kwargs["help_texts"] = help_texts + return super().get_form(request, obj, **kwargs) + def save_model(self, request, obj, form, change): """ Model-save hook. diff --git a/evennia/web/admin/tags.py b/evennia/web/admin/tags.py index f0c2b6ac0f..42cab804c1 100644 --- a/evennia/web/admin/tags.py +++ b/evennia/web/admin/tags.py @@ -16,6 +16,47 @@ from evennia.utils.dbserialize import from_pickle, _SaverSet class TagForm(forms.ModelForm): """ + Form to display fields in the stand-alone Tag display. + + """ + + db_key = forms.CharField( + label="Key/Name", required=True, help_text="The main key identifier" + ) + db_category = forms.CharField( + label="Category", + help_text="Used for grouping tags. Unset (default) gives a category of None", + required=False, + ) + db_tagtype = forms.ChoiceField( + label="Type", + choices=[(None, "-"), ("alias", "alias"), ("permission", "permission")], + help_text="Tags are used for different things. Unset for regular tags.", + required=False + ) + db_model = forms.ChoiceField( + label="Model" , + required=False, + help_text = "Each Tag can only 'attach' to one type of entity.", + choices=([("objectdb", "objectdb"), ("accountdb", "accountdb"), + ("scriptdb", "scriptdb"), ("channeldb", "channeldb"), + ("helpentry", "helpentry"), ("msg", "msg")]) + ) + db_data = forms.CharField( + label="Data", + help_text="Usually unused. Intended for info about the tag itself", + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), + required=False, + ) + + class Meta: + fields = ("tag_key", "tag_category", "tag_data", "tag_type") + + +class InlineTagForm(forms.ModelForm): + """ + Form for displaying tags inline together with other entities. + 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 @@ -32,13 +73,15 @@ class TagForm(forms.ModelForm): help_text="Used for grouping tags. Unset (default) gives a category of None", required=False, ) - tag_type = forms.CharField( + tag_type = forms.ChoiceField( label="Type", - help_text='Internal use. Either unset, "alias" or "permission"', - required=False, + choices=[(None, "-"), ("alias", "alias"), ("permission", "permission")], + help_text="Tags are used for different things. Unset for regular tags.", + required=False ) tag_data = forms.CharField( label="Data", + widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}), help_text="Usually unused. Intended for eventual info about the tag itself", required=False, ) @@ -99,6 +142,8 @@ class TagFormSet(forms.BaseInlineFormSet): Object, where the handler is an AliasHandler, PermissionsHandler, or TagHandler, based on the type of tag. """ + verbose_name = "Tag" + verbose_name_plural = "Tags" def save(self, commit=True): def get_handler(finished_object): @@ -137,7 +182,8 @@ class TagInline(admin.TabularInline): # Set this to the through model of your desired M2M when subclassing. model = None verbose_name = "Tag" - form = TagForm + verbose_name_plural = "Tags" + form = InlineTagForm formset = TagFormSet related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing # raw_id_fields = ('tag',) @@ -164,9 +210,25 @@ class TagInline(admin.TabularInline): 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_model", "db_data") fields = ("db_key", "db_category", "db_tagtype", "db_model", "db_data") list_filter = ("db_tagtype", "db_category", "db_model") + form = TagForm + + fieldsets = ( + ( + None, + { + "fields": ( + ("db_key", "db_category"), + ("db_tagtype", "db_model"), + "db_data" + ) + }, + ), + ) + diff --git a/evennia/web/admin/utils.py b/evennia/web/admin/utils.py new file mode 100644 index 0000000000..c0ff2f78b4 --- /dev/null +++ b/evennia/web/admin/utils.py @@ -0,0 +1,51 @@ +""" +Helper utils for admin views. + +""" + +import importlib +from evennia.utils.utils import get_all_typeclasses, inherits_from + + +def get_and_load_typeclasses(parent=None, excluded_parents=None): + """ + Get all typeclasses. We we need to initialize things here + for them to be actually available in the admin process. + This is intended to be used with forms.ChoiceField. + + Args: + parent (str or class, optional): Limit selection to this class and its children + (at any distance). + exclude (list): Class-parents to exclude from the resulting list. All + children of these paretns will be skipped. + + Returns: + list: A list of (str, str), the way ChoiceField wants it. + + """ + # this is necessary in order to have typeclasses imported and accessible + # in the inheritance tree. + import evennia + evennia._init() + + # this return a dict (path: class} + tmap = get_all_typeclasses(parent=parent) + + # filter out any excludes + excluded_parents = excluded_parents or [] + tpaths = [path for path, tclass in tmap.items() + if not any(inherits_from(tclass, excl) for excl in excluded_parents)] + + # sort so we get custom paths (not in evennia repo) first + tpaths = sorted(tpaths, key=lambda k: (1 if k.startswith("evennia.") else 0, k)) + + # the base models are not typeclasses so we filter them out + tpaths = [path for path in tpaths if path not in + ("evennia.objects.models.ObjectDB", + "evennia.accounts.models.AccountDB", + "evennia.scripts.models.ScriptDB", + "evennia.comms.models.ChannelDB",)] + + # return on form exceptedaccepted by ChoiceField + return [(path, path) for path in tpaths if path] +