Source code for evennia.web.admin.accounts

#
# 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.admin.utils import unquote
from django.contrib.admin.widgets import FilteredSelectMultiple, ForeignKeyRawIdWidget
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.views.decorators.debug import sensitive_post_parameters

from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB
from evennia.utils import create

from . import utils as adminutils
from .attributes import AttributeInline
from .tags import TagInline

sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())


# handle the custom User editor
[docs]class AccountChangeForm(UserChangeForm): """ Modify the accountdb class. """
[docs] 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.", ) 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.<BR>" "If your custom class is not found here, it may not be imported into Evennia yet.", choices=lambda: 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..." "<BR>(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, ) is_superuser = forms.BooleanField( label="Superuser status", required=False, help_text="Superusers bypass all in-game locks and has all " "permissions without explicitly assigning them. Usually " "only one superuser (user #1) is needed and only a superuser " "can create another superuser.<BR>" "Only Superusers can change the user/group permissions below.", )
[docs] 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"]
[docs] def __init__(self, *args, **kwargs): """ Tweak some fields dynamically. """ super().__init__(*args, **kwargs) # better help text for cmdset_storage 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 Accounts " f"is <strong>{account_cmdset}</strong> ." )
[docs]class AccountCreationForm(UserCreationForm): """ Create a new AccountDB instance. """
[docs] 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.", )
[docs] 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
[docs]class AccountTagInline(TagInline): """ Inline Account Tags. """ model = AccountDB.db_tags.through related_field = "accountdb"
[docs]class AccountAttributeInline(AttributeInline): """ Inline Account Attributes. """ model = AccountDB.db_attributes.through related_field = "accountdb"
[docs]class ObjectPuppetInline(admin.StackedInline): """ Inline creation of puppet-Object in Account. """ from .objects import ObjectCreateForm verbose_name = "Puppeted Object" model = ObjectDB view_on_site = False show_change_link = True # template = "admin/accounts/stacked.html" form = ObjectCreateForm fieldsets = ( ( None, { "fields": ( ("db_key", "db_typeclass_path"), ("db_location", "db_home", "db_destination"), "db_cmdset_storage", "db_lock_storage", ), "description": "Object currently puppeted by the account (note that this " "will go away if account logs out or unpuppets)", }, ), ) extra = 0 readonly_fields = ( "db_key", "db_typeclass_path", "db_destination", "db_location", "db_home", "db_account", "db_cmdset_storage", "db_lock_storage", ) # disable adding/deleting this inline - read-only!
[docs] def has_add_permission(self, request, obj=None): return False
[docs] def has_delete_permission(self, request, obj=None): return False
[docs]@admin.register(AccountDB) class AccountAdmin(BaseUserAdmin): """ This is the main creation screen for Users/accounts """ list_display = ( "id", "username", "is_staff", "is_superuser", "db_typeclass_path", "db_date_created", ) list_display_links = ("id", "username") form = AccountChangeForm add_form = AccountCreationForm search_fields = ["=id", "^username", "db_typeclass_path"] ordering = ["-db_date_created", "id"] list_filter = ["is_superuser", "is_staff", "db_typeclass_path"] inlines = [AccountTagInline, AccountAttributeInline] readonly_fields = ["db_date_created", "serialized_string", "puppeted_objects"] view_on_site = False fieldsets = ( ( None, { "fields": ( ("username", "db_typeclass_path"), "password", "email", "db_date_created", "db_lock_storage", "db_cmdset_storage", "puppeted_objects", "serialized_string", ) }, ), ( "Admin/Website properties", { "fields": ( ("first_name", "last_name"), "last_login", "date_joined", "is_active", "is_staff", "is_superuser", "user_permissions", "groups", ), "description": "<i>Used by the website/Django admin. " "Except for `superuser status`, the permissions are not used in-game.</i>", }, ), ) add_fieldsets = ( ( None, { "fields": ("username", "password1", "password2", "email"), "description": "<i>These account details are shared by the admin " "system and the game.</i>", }, ), )
[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 account there." )
[docs] def puppeted_objects(self, obj): """ Get any currently puppeted objects (read only list) """ return mark_safe( ", ".join( '<a href="{url}">{name}</a>'.format( url=reverse("admin:objects_objectdb_change", args=[obj.id]), name=obj.db_key ) for obj in ObjectDB.objects.filter(db_account=obj) ) )
puppeted_objects.help_text = ( "Objects currently puppeted by this Account. " "Link new ones from the `Objects` admin page.<BR>" "Note that these will disappear when a user unpuppets or goes offline - " "this is normal." )
[docs] 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 help_texts["puppeted_objects"] = self.puppeted_objects.help_text kwargs["help_texts"] = help_texts # security disabling for non-superusers form = super().get_form(request, obj, **kwargs) disabled_fields = set() if not request.user.is_superuser: disabled_fields |= {"is_superuser", "user_permissions", "user_groups"} for field_name in disabled_fields: if field_name in form.base_fields: form.base_fields[field_name].disabled = True return form
[docs] @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, )
[docs] 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()
[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:accounts_accountdb_change", args=[obj.id]))