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.utils.safestring import mark_safe
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.widgets import ForeignKeyRawIdWidget, FilteredSelectMultiple
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext as _
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.objects.models import ObjectDB
from evennia.accounts.models import AccountDB
from evennia.utils import create
from .attributes import AttributeInline
from .tags import TagInline
from . import utils as adminutils

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]))