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]
+