Correct admin in new location

This commit is contained in:
Griatch 2021-05-17 08:18:39 +02:00
parent 8e02be23e4
commit efe3a28343
15 changed files with 259 additions and 193 deletions

View file

@ -109,8 +109,8 @@ class AccountDB(TypedObject, AbstractUser):
__applabel__ = "accounts"
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
# class Meta:
# verbose_name = "Account"
class Meta:
verbose_name = "Account"
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch

View file

@ -378,7 +378,7 @@ class ObjectDB(TypedObject):
)
[o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()]
class Meta(object):
class Meta:
"""Define Django meta options"""
verbose_name = "Object"

View file

@ -161,6 +161,7 @@ class InMemoryAttribute(IAttribute):
class Attribute(IAttribute, SharedMemoryModel):
"""
This attribute is stored via Django. Most Attributes will be using this class.
"""
#
@ -219,7 +220,7 @@ class Attribute(IAttribute, SharedMemoryModel):
class Meta(object):
"Define Django meta options"
verbose_name = "Evennia Attribute"
verbose_name = "Attribute"
# Wrapper properties to easily set database fields. These are
# @property decorators that allows to access these fields using

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.3 on 2021-05-17 06:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('typeclasses', '0013_auto_20191015_1922'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='db_category',
field=models.CharField(blank=True, db_index=True, help_text='tag category', max_length=64, null=True, verbose_name='category'),
),
]

View file

@ -341,7 +341,7 @@ class TypedObject(SharedMemoryModel):
def nattributes(self):
return AttributeHandler(self, InMemoryAttributeBackend)
class Meta(object):
class Meta:
"""
Django setup info.
"""

View file

@ -53,7 +53,7 @@ class Tag(models.Model):
"key", max_length=255, null=True, help_text="tag identifier", db_index=True
)
db_category = models.CharField(
"category", max_length=64, null=True, help_text="tag category", db_index=True
"category", max_length=64, null=True, blank=True, help_text="tag category", db_index=True
)
db_data = models.TextField(
"data",

View file

@ -0,0 +1,14 @@
"""
Django-admin code for customizing the web admin for Evennia.
"""
# importing here are necessary for Django to find these, since it will only
# look for `admin` in the web/ folder.
from .accounts import AccountAdmin
from .objects import ObjectAdmin
from .scripts import ScriptAdmin
from .comms import ChannelAdmin, MsgAdmin
from .help import HelpEntryAdmin
from .tags import TagAdmin

View file

@ -19,14 +19,15 @@ 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
from .attributes import AttributeInline
from .tags import TagInline
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
# handle the custom User editor
class AccountDBChangeForm(UserChangeForm):
class AccountChangeForm(UserChangeForm):
"""
Modify the accountdb class.
@ -61,7 +62,7 @@ class AccountDBChangeForm(UserChangeForm):
return self.cleaned_data["username"]
class AccountDBCreationForm(UserCreationForm):
class AccountCreationForm(UserCreationForm):
"""
Create a new AccountDB instance.
"""
@ -214,14 +215,14 @@ class AccountAttributeInline(AttributeInline):
@admin.register(AccountDB)
class AccountDBAdmin(BaseUserAdmin):
class AccountAdmin(BaseUserAdmin):
"""
This is the main creation screen for Users/accounts
"""
list_display = ("username", "email", "is_staff", "is_superuser")
form = AccountDBChangeForm
add_form = AccountDBCreationForm
form = AccountChangeForm
add_form = AccountCreationForm
inlines = [AccountTagInline, AccountAttributeInline]
fieldsets = (
(None, {"fields": ("username", "password", "email")}),
@ -360,6 +361,3 @@ class AccountDBAdmin(BaseUserAdmin):
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))
# admin.site.register(AccountDB, AccountDBAdmin)

View file

@ -1,168 +1,21 @@
"""
Attribute admin.
Note that we don't present a separate admin for these, since they are only
relevant together with a specific object.
"""
import traceback
from datetime import datetime
from django.contrib import admin
from evennia.typeclasses.models import Tag
from evennia.typeclasses.attributes import Attribute
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.
@ -318,6 +171,7 @@ class AttributeInline(admin.TabularInline):
# Set this to the through model of your desired M2M when subclassing.
model = None
verbose_name = "Attribute"
form = AttributeForm
formset = AttributeFormSet
related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
@ -339,6 +193,3 @@ class AttributeInline(admin.TabularInline):
ProxyFormset.related_field = self.related_field
return ProxyFormset
admin.site.register(Tag, TagAdmin)

View file

@ -5,9 +5,11 @@ 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
from .attributes import AttributeInline
from .tags import TagInline
class ChannelAttributeInline(AttributeInline):
"""
@ -63,7 +65,7 @@ class ChannelAdmin(admin.ModelAdmin):
"""
inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ("id", "db_key", "db_lock_storage", "subscriptions")
list_display = ("id", "db_key", "no_of_subscribers", "db_lock_storage")
list_display_links = ("id", "db_key")
ordering = ["db_key"]
search_fields = ["id", "db_key", "db_tags__db_key"]
@ -95,6 +97,16 @@ class ChannelAdmin(admin.ModelAdmin):
"""
return ", ".join([str(sub) for sub in obj.subscriptions.all()])
def no_of_subscribers(self, obj):
"""
Get number of subs for a a channel .
Args:
obj (Channel): The channel to get subs from.
"""
return sum(1 for sub in obj.subscriptions.all())
def save_model(self, request, obj, form, change):
"""
Model-save hook.

View file

@ -4,10 +4,11 @@ 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
from evennia.accounts.models import AccountDB
@staff_member_required
def evennia_admin(request):

View file

@ -4,7 +4,8 @@ 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
from .tags import TagInline
class HelpTagInline(TagInline):

View file

@ -5,11 +5,13 @@
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 _
from evennia.objects.models import ObjectDB
from .attributes import AttributeInline
from .tags import TagInline
class ObjectAttributeInline(AttributeInline):
"""
@ -86,7 +88,8 @@ class ObjectEditForm(ObjectCreateForm):
)
class ObjectDBAdmin(admin.ModelAdmin):
@admin.register(ObjectDB)
class ObjectAdmin(admin.ModelAdmin):
"""
Describes the admin page for Objects.
@ -143,7 +146,7 @@ class ObjectDBAdmin(admin.ModelAdmin):
Args:
request (Request): Incoming request.
obj (ObjectDB, optional): Database object.
obj (Object, optional): Database object.
"""
if not obj:
return self.add_fieldsets
@ -192,6 +195,3 @@ class ObjectDBAdmin(admin.ModelAdmin):
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:objects_objectdb_change", args=[obj.id]))
admin.site.register(ObjectDB, ObjectDBAdmin)

View file

@ -3,11 +3,11 @@
# in the web admin interface.
#
from django.conf import settings
from evennia.typeclasses.admin import AttributeInline, TagInline
from django.contrib import admin
from evennia.scripts.models import ScriptDB
from django.contrib import admin
from .attributes import AttributeInline
from .tags import TagInline
class ScriptTagInline(TagInline):
@ -30,7 +30,8 @@ class ScriptAttributeInline(AttributeInline):
related_field = "scriptdb"
class ScriptDBAdmin(admin.ModelAdmin):
@admin.register(ScriptDB)
class ScriptAdmin(admin.ModelAdmin):
"""
Displaying the main Script page.
@ -86,6 +87,3 @@ class ScriptDBAdmin(admin.ModelAdmin):
# 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)

172
evennia/web/admin/tags.py Normal file
View file

@ -0,0 +1,172 @@
"""
Tag admin
"""
import traceback
from datetime import datetime
from django.contrib import admin
from evennia.typeclasses.tags import Tag
from django import forms
from evennia.utils.picklefield import PickledFormField
from evennia.utils.dbserialize import from_pickle, _SaverSet
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
verbose_name = "Tag"
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
@admin.register(Tag)
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")