diff --git a/evennia/accounts/admin.py b/evennia/accounts/admin.py index 7dbe34f93a..327d2b4eb2 100644 --- a/evennia/accounts/admin.py +++ b/evennia/accounts/admin.py @@ -4,13 +4,26 @@ # from django import forms from django.conf import settings -from django.contrib import admin +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): @@ -260,6 +273,71 @@ class AccountDBAdmin(BaseUserAdmin): ), ) + @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. diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index ef7ec2918b..b0e96a33d5 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -104,18 +104,24 @@ class TypeclassBase(SharedMemoryModelBase): attrs["typename"] = name attrs["path"] = "%s.%s" % (attrs["__module__"], name) - # typeclass proxy setup - app_label = None - # first check explicit __applabel__ on the typeclass - if "__applabel__" not in attrs: - # find the app-label in one of the bases, usually the dbmodel + def _get_dbmodel(bases): + """Recursively get the dbmodel""" + if not hasattr(bases, "__iter__"): + bases = [bases] for base in bases: - try: - attrs["__applabel__"] = base.__applabel__ - except AttributeError: - pass - else: - break + if base._meta.proxy or base._meta.abstract: + for kls in base._meta.parents: + return _get_dbmodel(kls) + return base + + dbmodel = _get_dbmodel(bases) + + # typeclass proxy setup + # first check explicit __applabel__ on the typeclass, then figure + # it out from the dbmodel + if dbmodel and "__applabel__" not in attrs: + # find the app-label in one of the bases, usually the dbmodel + attrs["__applabel__"] = dbmodel._meta.app_label if "Meta" not in attrs: class Meta: @@ -127,9 +133,20 @@ class TypeclassBase(SharedMemoryModelBase): new_class = ModelBase.__new__(cls, name, bases, attrs) + # django doesn't support inheriting proxy models so we hack support for + # it here by injecting `proxy_for_model` to the actual dbmodel. + # Unfortunately we cannot also set the correct model_name, because this + # would block multiple-inheritance of typeclasses (Django doesn't allow + # multiple bases of the same model). + if dbmodel: + new_class._meta.proxy_for_model = dbmodel + # Maybe Django will eventually handle this in the future: + # new_class._meta.model_name = dbmodel._meta.model_name + # attach signals signals.post_save.connect(call_at_first_save, sender=new_class) - signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class) + signals.pre_delete.connect( + remove_attributes_on_delete, sender=new_class) return new_class