diff --git a/evennia/accounts/models.py b/evennia/accounts/models.py index 734bab0276..0d879e6ab7 100644 --- a/evennia/accounts/models.py +++ b/evennia/accounts/models.py @@ -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 diff --git a/evennia/objects/models.py b/evennia/objects/models.py index 5c36452880..19a67b3c7b 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -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" diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 590a727463..9009c48b75 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -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 diff --git a/evennia/typeclasses/migrations/0014_alter_tag_db_category.py b/evennia/typeclasses/migrations/0014_alter_tag_db_category.py new file mode 100644 index 0000000000..d489f261d5 --- /dev/null +++ b/evennia/typeclasses/migrations/0014_alter_tag_db_category.py @@ -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'), + ), + ] diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 7cedd6f59c..204de23b9d 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -341,7 +341,7 @@ class TypedObject(SharedMemoryModel): def nattributes(self): return AttributeHandler(self, InMemoryAttributeBackend) - class Meta(object): + class Meta: """ Django setup info. """ diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index 8740e23254..ceb0d2f592 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -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", diff --git a/evennia/web/admin/__init__.py b/evennia/web/admin/__init__.py index e69de29bb2..9f0eb5392f 100644 --- a/evennia/web/admin/__init__.py +++ b/evennia/web/admin/__init__.py @@ -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 diff --git a/evennia/web/admin/accounts.py b/evennia/web/admin/accounts.py index 7161a0a9f7..2c5980d700 100644 --- a/evennia/web/admin/accounts.py +++ b/evennia/web/admin/accounts.py @@ -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) diff --git a/evennia/web/admin/typeclasses.py b/evennia/web/admin/attributes.py similarity index 53% rename from evennia/web/admin/typeclasses.py rename to evennia/web/admin/attributes.py index ed23c0a305..74617babc3 100644 --- a/evennia/web/admin/typeclasses.py +++ b/evennia/web/admin/attributes.py @@ -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) diff --git a/evennia/web/admin/comms.py b/evennia/web/admin/comms.py index dc84519437..fb87b0130e 100644 --- a/evennia/web/admin/comms.py +++ b/evennia/web/admin/comms.py @@ -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. diff --git a/evennia/web/admin/frontpage.py b/evennia/web/admin/frontpage.py index d34ace2d34..eb69465a28 100644 --- a/evennia/web/admin/frontpage.py +++ b/evennia/web/admin/frontpage.py @@ -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): diff --git a/evennia/web/admin/help.py b/evennia/web/admin/help.py index 709dfae2a6..2bed18c69e 100644 --- a/evennia/web/admin/help.py +++ b/evennia/web/admin/help.py @@ -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): diff --git a/evennia/web/admin/objects.py b/evennia/web/admin/objects.py index 59a1d85c68..2429e4f2d0 100644 --- a/evennia/web/admin/objects.py +++ b/evennia/web/admin/objects.py @@ -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) diff --git a/evennia/web/admin/scripts.py b/evennia/web/admin/scripts.py index 3991fbc0d5..d0cddff39b 100644 --- a/evennia/web/admin/scripts.py +++ b/evennia/web/admin/scripts.py @@ -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) diff --git a/evennia/web/admin/tags.py b/evennia/web/admin/tags.py new file mode 100644 index 0000000000..f0c2b6ac0f --- /dev/null +++ b/evennia/web/admin/tags.py @@ -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")