Source code for evennia.web.admin.objects
#
# 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, messages
from django.contrib.admin.utils import flatten_fieldsets
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.http import HttpResponseRedirect
from django.urls import path, reverse
from django.utils.html import format_html
from django.utils.translation import gettext as _
from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB
from . import utils as adminutils
from .attributes import AttributeInline
from .tags import TagInline
[docs]class ObjectAttributeInline(AttributeInline):
"""
Defines inline descriptions of Attributes (experimental)
"""
model = ObjectDB.db_attributes.through
related_field = "objectdb"
[docs]class ObjectTagInline(TagInline):
"""
Defines inline descriptions of Tags (experimental)
"""
model = ObjectDB.db_tags.through
related_field = "objectdb"
[docs]class ObjectCreateForm(forms.ModelForm):
"""
This form details the look of the fields.
"""
db_key = forms.CharField(
label="Name/Key",
widget=forms.TextInput(attrs={"size": "78"}),
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.ChoiceField(
label="Typeclass",
initial={settings.BASE_OBJECT_TYPECLASS: settings.BASE_OBJECT_TYPECLASS},
help_text="This is the Python-path to the class implementing the actual functionality. "
f"<BR>If you are creating a Character you usually need <B>{settings.BASE_CHARACTER_TYPECLASS}</B> "
"or a subclass of that. <BR>If your custom class is not found in the list, it may not be imported "
"into Evennia yet.",
choices=lambda: adminutils.get_and_load_typeclasses(parent=ObjectDB),
)
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 "
"<i>type:lockfunction(args);type2:lockfunction2(args);...",
)
db_cmdset_storage = forms.CharField(
label="CmdSet",
initial="",
required=False,
widget=forms.TextInput(attrs={"size": "78"}),
)
# This is not working well because it will not properly allow an empty choice, and will
# also not work well for comma-separated storage without more work. Notably, it's also
# a bit hard to visualize.
# db_cmdset_storage = forms.MultipleChoiceField(
# label="CmdSet",
# required=False,
# choices=adminutils.get_and_load_typeclasses(parent=ObjectDB))
db_location = forms.ModelChoiceField(
ObjectDB.objects.all(),
label="Location",
required=False,
widget=ForeignKeyRawIdWidget(
ObjectDB._meta.get_field("db_location").remote_field, admin.site
),
help_text="The (current) in-game location.<BR>"
"Usually a Room but can be<BR>"
"empty for un-puppeted Characters.",
)
db_home = forms.ModelChoiceField(
ObjectDB.objects.all(),
label="Home",
required=False,
widget=ForeignKeyRawIdWidget(
ObjectDB._meta.get_field("db_location").remote_field, admin.site
),
help_text="Fallback in-game location.<BR>"
"All objects should usually have<BR>"
"a home location.",
)
db_destination = forms.ModelChoiceField(
ObjectDB.objects.all(),
label="Destination",
required=False,
widget=ForeignKeyRawIdWidget(
ObjectDB._meta.get_field("db_destination").remote_field, admin.site
),
help_text="Only used by Exits.",
)
[docs] def __init__(self, *args, **kwargs):
"""
Tweak some fields dynamically.
"""
super().__init__(*args, **kwargs)
# set default home
home_id = str(settings.DEFAULT_HOME)
home_id = home_id[1:] if home_id.startswith("#") else home_id
default_home = ObjectDB.objects.filter(id=home_id)
if default_home:
default_home = default_home[0]
self.fields["db_home"].initial = default_home
self.fields["db_location"].initial = default_home
# better help text for cmdset_storage
char_cmdset = settings.CMDSET_CHARACTER
account_cmdset = settings.CMDSET_ACCOUNT
self.fields["db_cmdset_storage"].help_text = (
"Path to Command-set path. Most non-character objects don't need a cmdset"
" and can leave this field blank. Default cmdset-path<BR> for Characters "
f"is <strong>{char_cmdset}</strong> ."
)
[docs]class ObjectEditForm(ObjectCreateForm):
"""
Form used for editing. Extends the create one with more fields
"""
db_account = forms.ModelChoiceField(
AccountDB.objects.all(),
label="Puppeting Account",
required=False,
widget=ForeignKeyRawIdWidget(
ObjectDB._meta.get_field("db_account").remote_field, admin.site
),
help_text="An Account puppeting this Object (if any).<BR>Note that when a user logs "
"off/unpuppets, this<BR>field will be empty again. This is normal.",
)
[docs]@admin.register(ObjectDB)
class ObjectAdmin(admin.ModelAdmin):
"""
Describes the admin page for Objects.
"""
inlines = [ObjectTagInline, ObjectAttributeInline]
list_display = (
"id",
"db_key",
"db_typeclass_path",
"db_location",
"db_destination",
"db_account",
"db_date_created",
)
list_display_links = ("id", "db_key")
ordering = ["-db_date_created", "-id"]
search_fields = [
"=id",
"^db_key",
"db_typeclass_path",
"^db_account__db_key",
"^db_location__db_key",
]
raw_id_fields = ("db_destination", "db_location", "db_home", "db_account")
readonly_fields = ("serialized_string", "link_button")
save_as = True
save_on_top = True
list_select_related = True
view_on_site = False
list_filter = ("db_typeclass_path",)
# editing fields setup
form = ObjectEditForm
fieldsets = (
(
None,
{
"fields": (
("db_key", "db_typeclass_path"),
("db_location", "db_home", "db_destination"),
("db_account", "link_button"),
"db_cmdset_storage",
"db_lock_storage",
"serialized_string",
)
},
),
)
add_form = ObjectCreateForm
add_fieldsets = (
(
None,
{
"fields": (
("db_key", "db_typeclass_path"),
("db_location", "db_home", "db_destination"),
"db_cmdset_storage",
)
},
),
)
[docs] 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 this object there."
)
[docs] def get_fieldsets(self, request, obj=None):
"""
Return fieldsets.
Args:
request (Request): Incoming request.
obj (Object, optional): Database object.
"""
if not obj:
return self.add_fieldsets
return super().get_fieldsets(request, obj)
[docs] def get_form(self, request, obj=None, **kwargs):
"""
Use special form during creation.
Args:
request (Request): Incoming request.
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)
return super().get_form(request, obj, **defaults)
[docs] def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"account-object-link/<int:pk>",
self.admin_site.admin_view(self.link_object_to_account),
name="object-account-link",
)
]
return custom_urls + urls
[docs] def link_button(self, obj):
return format_html(
'<a class="button" href="{}">Link to Account</a> ',
reverse("admin:object-account-link", args=[obj.pk]),
)
link_button.short_description = "Create attrs/locks for puppeting"
link_button.allow_tags = True
[docs] def link_object_to_account(self, request, object_id):
"""
Link object and account when pressing the button.
This will:
- Set account.db._last_puppet to this object
- Add object to account.characters
- Change object locks to allow puppeting by account
"""
obj = self.get_object(request, object_id)
account = obj.db_account
if account:
account.db._last_puppet = obj
account.characters.add(obj)
if not obj.access(account, "puppet"):
lock = obj.locks.get("puppet")
lock += f" or pid({account.id})"
obj.locks.add(lock)
self.message_user(
request,
"Did the following (where possible): "
f"Set Account.db._last_puppet = {obj}, "
f"Added {obj} to Account.characters list, "
f"Added 'puppet:pid({account.id})' lock to {obj}.",
)
else:
self.message_user(
request,
"Account must be connected for this action "
"(set Puppeting Account and save this page first).",
level=messages.ERROR,
)
# stay on the same page
return HttpResponseRedirect(reverse("admin:objects_objectdb_change", args=[obj.pk]))
[docs] def save_model(self, request, obj, form, change):
"""
Model-save hook.
Args:
request (Request): Incoming request.
obj (Object): Database object.
form (Form): Form instance.
change (bool): If this is a change or a new object.
"""
if not change:
# adding a new object
# have to call init with typeclass passed to it
obj.set_class_from_typeclass(typeclass_path=obj.db_typeclass_path)
obj.save()
obj.basetype_setup()
obj.basetype_posthook_setup()
obj.at_object_creation()
else:
obj.save()
obj.at_init()
[docs] def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:objects_objectdb_change", args=[obj.id]))