Trying to relocate admin (not working yet)

This commit is contained in:
Griatch 2021-05-16 23:40:31 +02:00
parent 273cc31146
commit 8e02be23e4
15 changed files with 67 additions and 49 deletions

View file

View file

@ -0,0 +1,365 @@
#
# 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.options import IS_POPUP_VAR
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.admin.utils import unquote
from django.template.response import TemplateResponse
from django.http import Http404, HttpResponseRedirect
from django.core.exceptions import PermissionDenied
from django.views.decorators.debug import sensitive_post_parameters
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.urls import path, reverse
from django.contrib.auth import update_session_auth_hash
from evennia.accounts.models import AccountDB
from evennia.typeclasses.admin import AttributeInline, TagInline
from evennia.utils import create
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
# handle the custom User editor
class AccountDBChangeForm(UserChangeForm):
"""
Modify the accountdb class.
"""
class Meta(object):
model = AccountDB
fields = "__all__"
username = forms.RegexField(
label="Username",
max_length=30,
regex=r"^[\w. @+-]+$",
widget=forms.TextInput(attrs={"size": "30"}),
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):
"""
Clean the username and check its existence.
"""
username = self.cleaned_data["username"]
if username.upper() == self.instance.username.upper():
return username
elif AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError("An account with that name " "already exists.")
return self.cleaned_data["username"]
class AccountDBCreationForm(UserCreationForm):
"""
Create a new AccountDB instance.
"""
class Meta(object):
model = AccountDB
fields = "__all__"
username = forms.RegexField(
label="Username",
max_length=30,
regex=r"^[\w. @+-]+$",
widget=forms.TextInput(attrs={"size": "30"}),
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):
"""
Cleanup username.
"""
username = self.cleaned_data["username"]
if AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError("An account with that name already " "exists.")
return username
class AccountForm(forms.ModelForm):
"""
Defines how to display Accounts
"""
class Meta(object):
model = AccountDB
fields = "__all__"
app_label = "accounts"
db_key = forms.RegexField(
label="Username",
initial="AccountDummy",
max_length=30,
regex=r"^[\w. @+-]+$",
required=False,
widget=forms.TextInput(attrs={"size": "30"}),
error_messages={
"invalid": "This value may contain only letters, spaces, numbers"
" and @/./+/-/_ characters."
},
help_text="This should be the same as the connected Account's key "
"name. 30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.",
)
db_typeclass_path = forms.CharField(
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.",
)
db_permissions = forms.CharField(
label="Permissions",
initial=settings.PERMISSION_ACCOUNT_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 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.",
)
db_lock_storage = forms.CharField(
label="Locks",
widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}),
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);...",
)
db_cmdset_storage = forms.CharField(
label="cmdset",
initial=settings.CMDSET_ACCOUNT,
widget=forms.TextInput(attrs={"size": "78"}),
required=False,
help_text="python path to account cmdset class (set in "
"settings.CMDSET_ACCOUNT by default)",
)
class AccountInline(admin.StackedInline):
"""
Inline creation of Account
"""
model = AccountDB
template = "admin/accounts/stacked.html"
form = AccountForm
fieldsets = (
(
"In-game Permissions and Locks",
{
"fields": ("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>",
},
),
(
"In-game Account data",
{
"fields": ("db_typeclass_path", "db_cmdset_storage"),
"description": "<i>These fields define in-game-specific properties "
"for the Account object in-game.</i>",
},
),
)
extra = 1
max_num = 1
class AccountTagInline(TagInline):
"""
Inline Account Tags.
"""
model = AccountDB.db_tags.through
related_field = "accountdb"
class AccountAttributeInline(AttributeInline):
"""
Inline Account Attributes.
"""
model = AccountDB.db_attributes.through
related_field = "accountdb"
@admin.register(AccountDB)
class AccountDBAdmin(BaseUserAdmin):
"""
This is the main creation screen for Users/accounts
"""
list_display = ("username", "email", "is_staff", "is_superuser")
form = AccountDBChangeForm
add_form = AccountDBCreationForm
inlines = [AccountTagInline, AccountAttributeInline]
fieldsets = (
(None, {"fields": ("username", "password", "email")}),
(
"Website profile",
{
"fields": ("first_name", "last_name"),
"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>",
},
),
(
"Website Permissions",
{
"fields": ("is_active", "is_staff", "is_superuser", "user_permissions", "groups"),
"description": "<i>These are permissions/permission groups for "
"accessing the admin site. They are unrelated to "
"in-game access rights.</i>",
},
),
(
"Game Options",
{
"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 = (
(
None,
{
"fields": ("username", "password1", "password2", "email"),
"description": "<i>These account details are shared by the admin "
"system and the game.</i>",
},
),
)
@sensitive_post_parameters_m
def user_change_password(self, request, id, form_url=""):
user = self.get_object(request, unquote(id))
if not self.has_change_permission(request, user):
raise PermissionDenied
if user is None:
raise Http404("%(name)s object with primary key %(key)r does not exist.") % {
"name": self.model._meta.verbose_name,
"key": escape(id),
}
if request.method == "POST":
form = self.change_password_form(user, request.POST)
if form.is_valid():
form.save()
change_message = self.construct_change_message(request, form, None)
self.log_change(request, user, change_message)
msg = "Password changed successfully."
messages.success(request, msg)
update_session_auth_hash(request, form.user)
return HttpResponseRedirect(
reverse(
"%s:%s_%s_change"
% (
self.admin_site.name,
user._meta.app_label,
# the model_name is something we need to hardcode
# since our accountdb is a proxy:
"accountdb",
),
args=(user.pk,),
)
)
else:
form = self.change_password_form(user)
fieldsets = [(None, {"fields": list(form.base_fields)})]
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
context = {
"title": "Change password: %s" % escape(user.get_username()),
"adminForm": adminForm,
"form_url": form_url,
"form": form,
"is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET),
"add": True,
"change": False,
"has_delete_permission": False,
"has_change_permission": True,
"has_absolute_url": False,
"opts": self.model._meta,
"original": user,
"save_as": False,
"show_save": True,
**self.admin_site.each_context(request),
}
request.current_app = self.admin_site.name
return TemplateResponse(
request,
self.change_user_password_template or "admin/auth/user/change_password.html",
context,
)
def save_model(self, request, obj, form, change):
"""
Custom save actions.
Args:
request (Request): Incoming request.
obj (Object): Object to save.
form (Form): Related form instance.
change (bool): False if this is a new save and not an update.
"""
obj.save()
if not change:
# calling hooks for new account
obj.set_class_from_typeclass(typeclass_path=settings.BASE_ACCOUNT_TYPECLASS)
obj.basetype_setup()
obj.at_account_creation()
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))
# admin.site.register(AccountDB, AccountDBAdmin)

123
evennia/web/admin/comms.py Normal file
View file

@ -0,0 +1,123 @@
"""
This defines how Comm models are displayed in the web admin interface.
"""
from django.contrib import admin
from evennia.comms.models import ChannelDB
from evennia.typeclasses.admin import AttributeInline, TagInline
from django.conf import settings
class ChannelAttributeInline(AttributeInline):
"""
Inline display of Channel Attribute - experimental
"""
model = ChannelDB.db_attributes.through
related_field = "channeldb"
class ChannelTagInline(TagInline):
"""
Inline display of Channel Tags - experimental
"""
model = ChannelDB.db_tags.through
related_field = "channeldb"
class MsgAdmin(admin.ModelAdmin):
"""
Defines display for Msg objects
"""
list_display = (
"id",
"db_date_created",
"db_sender",
"db_receivers",
"db_channels",
"db_message",
"db_lock_storage",
)
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)
class ChannelAdmin(admin.ModelAdmin):
"""
Defines display for Channel objects
"""
inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ("id", "db_key", "db_lock_storage", "subscriptions")
list_display_links = ("id", "db_key")
ordering = ["db_key"]
search_fields = ["id", "db_key", "db_tags__db_key"]
save_as = True
save_on_top = True
list_select_related = True
raw_id_fields = ("db_object_subscriptions", "db_account_subscriptions")
fieldsets = (
(
None,
{
"fields": (
("db_key",),
"db_lock_storage",
"db_account_subscriptions",
"db_object_subscriptions",
)
},
),
)
def subscriptions(self, obj):
"""
Helper method to get subs from a channel.
Args:
obj (Channel): The channel to get subs from.
"""
return ", ".join([str(sub) for sub in obj.subscriptions.all()])
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.
"""
obj.save()
if not change:
# adding a new object
# have to call init with typeclass passed to it
obj.set_class_from_typeclass(typeclass_path=settings.BASE_CHANNEL_TYPECLASS)
obj.at_init()
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id]))
admin.site.register(ChannelDB, ChannelAdmin)

View file

@ -0,0 +1,26 @@
"""
Admin views.
"""
from django.contrib.admin.sites import site
from evennia.accounts.models import AccountDB
from django.shortcuts import render
from django.contrib.admin.views.decorators import staff_member_required
@staff_member_required
def evennia_admin(request):
"""
Helpful Evennia-specific admin page.
"""
return render(request, "admin/frontpage.html", {"accountdb": AccountDB})
def admin_wrapper(request):
"""
Wrapper that allows us to properly use the base Django admin site, if needed.
"""
return staff_member_required(site.index)(request)

56
evennia/web/admin/help.py Normal file
View file

@ -0,0 +1,56 @@
"""
This defines how to edit help entries in Admin.
"""
from django import forms
from django.contrib import admin
from evennia.help.models import HelpEntry
from evennia.typeclasses.admin import TagInline
class HelpTagInline(TagInline):
model = HelpEntry.db_tags.through
related_field = "helpentry"
class HelpEntryForm(forms.ModelForm):
"Defines how to display the help entry"
class Meta(object):
model = HelpEntry
fields = "__all__"
db_help_category = forms.CharField(
label="Help category", initial="General", help_text="organizes help entries in lists"
)
db_lock_storage = forms.CharField(
label="Locks",
initial="view:all()",
required=False,
widget=forms.TextInput(attrs={"size": "40"}),
)
class HelpEntryAdmin(admin.ModelAdmin):
"Sets up the admin manaager for help entries"
inlines = [HelpTagInline]
list_display = ("id", "db_key", "db_help_category", "db_lock_storage")
list_display_links = ("id", "db_key")
search_fields = ["^db_key", "db_entrytext"]
ordering = ["db_help_category", "db_key"]
save_as = True
save_on_top = True
list_select_related = True
form = HelpEntryForm
fieldsets = (
(
None,
{
"fields": (("db_key", "db_help_category"), "db_entrytext", "db_lock_storage"),
"description": "Sets a Help entry. Set lock to <i>view:all()</I> unless you want to restrict it.",
},
),
)
admin.site.register(HelpEntry, HelpEntryAdmin)

View file

@ -0,0 +1,197 @@
#
# 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.typeclasses.admin import AttributeInline, TagInline
from evennia.objects.models import ObjectDB
from django.contrib.admin.utils import flatten_fieldsets
from django.utils.translation import gettext as _
class ObjectAttributeInline(AttributeInline):
"""
Defines inline descriptions of Attributes (experimental)
"""
model = ObjectDB.db_attributes.through
related_field = "objectdb"
class ObjectTagInline(TagInline):
"""
Defines inline descriptions of Tags (experimental)
"""
model = ObjectDB.db_tags.through
related_field = "objectdb"
class ObjectCreateForm(forms.ModelForm):
"""
This form details the look of the fields.
"""
class Meta(object):
model = ObjectDB
fields = "__all__"
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.CharField(
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.",
)
db_cmdset_storage = forms.CharField(
label="CmdSet",
initial="",
required=False,
widget=forms.TextInput(attrs={"size": "78"}),
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")
class ObjectEditForm(ObjectCreateForm):
"""
Form used for editing. Extends the create one with more fields
"""
class Meta(object):
fields = "__all__"
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);...",
)
class ObjectDBAdmin(admin.ModelAdmin):
"""
Describes the admin page for Objects.
"""
inlines = [ObjectTagInline, ObjectAttributeInline]
list_display = ("id", "db_key", "db_account", "db_typeclass_path")
list_display_links = ("id", "db_key")
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")
save_as = True
save_on_top = True
list_select_related = True
list_filter = ("db_typeclass_path",)
# editing fields setup
form = ObjectEditForm
fieldsets = (
(
None,
{
"fields": (
("db_key", "db_typeclass_path"),
("db_lock_storage",),
("db_location", "db_home"),
"db_destination",
"db_cmdset_storage",
)
},
),
)
add_form = ObjectCreateForm
add_fieldsets = (
(
None,
{
"fields": (
("db_key", "db_typeclass_path"),
("db_location", "db_home"),
"db_destination",
"db_cmdset_storage",
)
},
),
)
def get_fieldsets(self, request, obj=None):
"""
Return fieldsets.
Args:
request (Request): Incoming request.
obj (ObjectDB, optional): Database object.
"""
if not obj:
return self.add_fieldsets
return super().get_fieldsets(request, obj)
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during creation.
Args:
request (Request): Incoming request.
obj (Object, optional): Database object.
"""
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)
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.
"""
obj.save()
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.basetype_setup()
obj.basetype_posthook_setup()
obj.at_object_creation()
obj.at_init()
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]))
admin.site.register(ObjectDB, ObjectDBAdmin)

View file

@ -0,0 +1,91 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.conf import settings
from evennia.typeclasses.admin import AttributeInline, TagInline
from evennia.scripts.models import ScriptDB
from django.contrib import admin
class ScriptTagInline(TagInline):
"""
Inline script tags.
"""
model = ScriptDB.db_tags.through
related_field = "scriptdb"
class ScriptAttributeInline(AttributeInline):
"""
Inline attribute tags.
"""
model = ScriptDB.db_attributes.through
related_field = "scriptdb"
class ScriptDBAdmin(admin.ModelAdmin):
"""
Displaying the main Script page.
"""
list_display = (
"id",
"db_key",
"db_typeclass_path",
"db_obj",
"db_interval",
"db_repeats",
"db_persistent",
)
list_display_links = ("id", "db_key")
ordering = ["db_obj", "db_typeclass_path"]
search_fields = ["^db_key", "db_typeclass_path"]
save_as = True
save_on_top = True
list_select_related = True
raw_id_fields = ("db_obj",)
fieldsets = (
(
None,
{
"fields": (
("db_key", "db_typeclass_path"),
"db_interval",
"db_repeats",
"db_start_delay",
"db_persistent",
"db_obj",
)
},
),
)
inlines = [ScriptTagInline, ScriptAttributeInline]
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.
"""
obj.save()
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)
admin.site.register(ScriptDB, ScriptDBAdmin)

View file

@ -0,0 +1,344 @@
import traceback
from datetime import datetime
from django.contrib import admin
from evennia.typeclasses.models import Tag
from django import forms
from evennia.utils.picklefield import PickledFormField
from evennia.utils.dbserialize import from_pickle, _SaverSet
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")
fields = ("db_key", "db_category", "db_tagtype", "db_data")
list_filter = ("db_tagtype",)
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", required=True, help_text="This is the main key identifier"
)
tag_category = forms.CharField(
label="Category",
help_text="Used for grouping tags. Unset (default) gives a category of None",
required=False,
)
tag_type = forms.CharField(
label="Type",
help_text='Internal use. Either unset, "alias" or "permission"',
required=False,
)
tag_data = forms.CharField(
label="Data",
help_text="Usually unused. Intended for eventual info about the tag itself",
required=False,
)
class Meta:
fields = ("tag_key", "tag_category", "tag_data", "tag_type")
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().__init__(*args, **kwargs)
tagkey = None
tagcategory = None
tagtype = None
tagdata = None
if hasattr(self.instance, "tag"):
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().save(commit=False)
instance = self.instance
instance.tag_key = self.cleaned_data["tag_key"]
instance.tag_category = self.cleaned_data["tag_category"] or None
instance.tag_type = self.cleaned_data["tag_type"] or None
instance.tag_data = self.cleaned_data["tag_data"] or None
return instance
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):
def get_handler(finished_object):
related = getattr(finished_object, self.related_field)
try:
tagtype = finished_object.tag_type
except AttributeError:
tagtype = finished_object.tag.db_tagtype
if tagtype == "alias":
handler_name = "aliases"
elif tagtype == "permission":
handler_name = "permissions"
else:
handler_name = "tags"
return getattr(related, handler_name)
instances = super().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.tag_key, category=obj.tag_category)
for instance in instances:
handler = get_handler(instance)
handler.add(instance.tag_key, category=instance.tag_category, data=instance.tag_data)
class TagInline(admin.TabularInline):
"""
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', 'accountdb', etc.
"""
# Set this to the through model of your desired M2M when subclassing.
model = None
form = TagForm
formset = TagFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
# raw_id_fields = ('tag',)
# readonly_fields = ('tag',)
extra = 0
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().get_formset(request, obj, **kwargs)
class ProxyFormset(formset):
pass
ProxyFormset.related_field = self.related_field
return ProxyFormset
class AttributeForm(forms.ModelForm):
"""
This form overrides the base behavior of the ModelForm that would be used for a Attribute-through-model.
Since the through-models only have access to the foreignkeys of the Attribute and the Object that they're
attached to, we need to spoof the behavior of it being a form that would correspond to its Attribute,
or the creation of an Attribute. Instead of being saved, we'll call to the Object's handler, which will handle
the creation, change, or deletion of an Attribute 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", required=False, initial="Enter Attribute Name Here"
)
attr_category = forms.CharField(
label="Category", help_text="type of attribute, for sorting", required=False, max_length=128
)
attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False)
attr_type = forms.CharField(
label="Type",
help_text='Internal use. Either unset (normal Attribute) or "nick"',
required=False,
max_length=16,
)
attr_lockstring = forms.CharField(
label="Locks",
required=False,
help_text="Lock string on the form locktype:lockdef;lockfunc:lockdef;...",
widget=forms.Textarea(attrs={"rows": 1, "cols": 8}),
)
class Meta:
fields = ("attr_key", "attr_value", "attr_category", "attr_lockstring", "attr_type")
def __init__(self, *args, **kwargs):
"""
If we have an Attribute, then we'll prepopulate our instance with the fields we'd expect it
to have based on the Attribute. attr_key, attr_category, attr_value, attr_type,
and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
similarly be populated.
"""
super().__init__(*args, **kwargs)
attr_key = None
attr_category = None
attr_value = 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_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_lockstring"].initial = attr_lockstring
self.instance.attr_key = attr_key
self.instance.attr_category = attr_category
self.instance.attr_value = attr_value
# prevent from being transformed to str
if isinstance(attr_value, (set, _SaverSet)):
self.fields["attr_value"].disabled = True
self.instance.deserialized_value = from_pickle(attr_value)
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 an Attribute for the Handler that will be called
instance = self.instance
instance.attr_key = self.cleaned_data["attr_key"] or "no_name_entered_for_attribute"
instance.attr_category = self.cleaned_data["attr_category"] or None
instance.attr_value = self.cleaned_data["attr_value"]
# convert the serialized string value into an object, if necessary, for AttributeHandler
instance.attr_value = from_pickle(instance.attr_value)
instance.attr_type = self.cleaned_data["attr_type"] or None
instance.attr_lockstring = self.cleaned_data["attr_lockstring"]
return instance
def clean_attr_value(self):
"""
Prevent certain data-types from being cleaned due to literal_eval
failing on them. Otherwise they will be turned into str.
"""
data = self.cleaned_data["attr_value"]
initial = self.instance.attr_value
if isinstance(initial, (set, _SaverSet, datetime)):
return initial
return data
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().save(commit=False)
for obj in self.deleted_objects:
# self.deleted_objects is a list created when super of save is called, we'll remove those
handler = get_handler(obj)
handler.remove(obj.attr_key, category=obj.attr_category)
for instance in instances:
handler = get_handler(instance)
value = instance.attr_value
try:
handler.add(
instance.attr_key,
value,
category=instance.attr_category,
strattr=False,
lockstring=instance.attr_lockstring,
)
except (TypeError, ValueError):
# catch errors in nick templates and continue
traceback.print_exc()
continue
class AttributeInline(admin.TabularInline):
"""
A handler for inline Attributes. 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', 'accountdb', etc.
"""
# Set this to the through model of your desired M2M when subclassing.
model = None
form = AttributeForm
formset = AttributeFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
# raw_id_fields = ('attribute',)
# readonly_fields = ('attribute',)
extra = 0
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().get_formset(request, obj, **kwargs)
class ProxyFormset(formset):
pass
ProxyFormset.related_field = self.related_field
return ProxyFormset
admin.site.register(Tag, TagAdmin)

30
evennia/web/admin/urls.py Normal file
View file

@ -0,0 +1,30 @@
"""
Rerouting admin frontpage to evennia version.
These patterns are all under the admin/* namespace.
"""
from django.conf import settings
from django.contrib import admin
from django.conf.urls import url, include
from . import frontpage
urlpatterns = [
# Django original admin page. Make this URL is always available, whether
# we've chosen to use Evennia's custom admin or not.
url(r"/django/", frontpage.admin_wrapper, name="django_admin"),
# Admin docs
url(r"/doc/", include("django.contrib.admindocs.urls")),
]
if settings.EVENNIA_ADMIN:
urlpatterns += [
# Our override for the admin.
url("^/$", frontpage.evennia_admin, name="evennia_admin"),
# Makes sure that other admin pages get loaded.
url(r"^/", admin.site.urls),
]
else:
# Just include the normal Django admin.
urlpatterns += [url(r"^/", admin.site.urls)]