一、整表只读
对于readonly_fields是对单个字段设置只读,现在要对整个表使用只读,也做成可配置的。在自己项目的admin.py中进行配置,如在mytestapp_admin.py中对Customer进行整表只读配置,在基类BaseAdmin中增加readonly_table = False,即默认所有表都是可读写的,在CustomerAdmin中配置readonly_table = True。
此时,前端,在修改和添加页面中,将对应的保存、删除按钮隐去,使前端没有保存、删除按钮:
{% if not admin_class.readonly_table %}<div class="form-group row" ><div class="col-sm-2"><button type="button" class="btn btn-danger pull-left"><a href="/mytestapp/plcrm/customer/{{ form_obj.instance.id }}/delete/">Delete</a></button></div><div class="col-sm-3"></div><div class="col-sm-7 pull-right"><button type="submit" class="btn btn-success pull-right">Save</button></div></div>
{% endif %}
在记录查看页面中将Add按钮也隐去:
{% if not admin_class.readonly_table %}<div style="float: right"><a class="pull-right" href="{{ url_path }}add/">Add</a> </div>
{% endif %}
前端的隐去是可以通过技术手段再加上的,所以还需要在后端做验证。
对于修改和增加,在默认验证中,如果判断是只读表,直接抛出错误,在create_model_form中的default_clean函数中,增加:
if admin_class.readonly_table:raise ValidationError(_('Table is readonly,cannot be Modified or Add'),code='invalid',)
对于删除,在视图函数中进行判断:
def rec_obj_delete(req,app_name,table_name,id_num):admin_class = mytestapp_admin.enable_admins[app_name][table_name]model_obj = admin_class.model.objects.filter(id=id_num)print('{{{{{{{{',model_obj)if admin_class.readonly_table: # 如果配置了只读表,不能删除,添加错误显示信息error = {'readonlyerror':"Table is readonly,[%s] cannot be deleted!"%(model_obj)}else:error = {}if req.method == "POST":if not admin_class.readonly_table: # 判断是否只读表,不是,可以删除,否则不能删除model_obj.delete()return redirect("/mytestapp/%s/%s/" %(app_name,table_name))return render(req,"mytestapp/rec_delete.html",{"model_obj":model_obj,'error':error})
然后在前端页面中加上{{ error }},显示错误信息就可以了。
对于自定义的动作中的删除,再对应的视图函数中加上判断:
error = ""if req.method == "POST":if not admin_class.readonly_table:select_ids = req.POST.get('selected_ids')select_ids = select_ids.split(',')actions = req.POST.get('myaction')action_obj = getattr(admin_class,actions)models_objs = admin_class.model.objects.filter(id__in=select_ids)action_obj(admin_class,req,models_objs)else:error = 'readonlyerror:Table is readonly, cannot be deleted!'
当然,对于自定制的动作,本身就是特殊的操作,也可以根据需要不进行只读表判断,可以进行正常删除。
实现URL跳转:
对于菜单项,如客户首页,想跳转到前面写的mytestapp/plcrm/customer项,在对应的页面中增加:
{% block page-content %}<script>window.location.assign("{% url 'table_objs' 'plcrm' 'customer' %}");</script> {% endblock %}
点击后自动跳转的相应页面。
二、自定义用户认证
项目中使用的用户,是借助于admin中的User,并自己定义了一个UserProfile,进行了一对一关联,在自定义的UserProfile中对用户进行自定义配置。这种方法的弊端是在添加用户时,需要先在User表中添加一条,再在UserProfile中添加,两次操作,很麻烦。
现在要写一个model,模拟django.contrib.auth.models.User,即admin中的模型,并最终使用自定义的这个模型来做用户验证,即实现User的作用。
首先模拟User做一个新的模型,User是继承了AbstractUser:class User(AbstractUser),而AbstractUser又继承了AbstractBaseUser, PermissionsMixin,即class AbstractUser(AbstractBaseUser, PermissionsMixin),AbstractBaseUser定义用户,PermissionsMixin定义权限。我们自定义的模型,也继承AbstractBaseUser, PermissionsMixin:
admin原来的用户管理:
登录,注意是账户名是:Username
用户表是在AUTHENTICATION AND AUTHORAZATION中
用户相关信息:
对应的User定义: class User(AbstractUser),而AbstractUser如下
class AbstractUser(AbstractBaseUser, PermissionsMixin):"""An abstract base class implementing a fully featured User model withadmin-compliant permissions.Username and password are required. Other fields are optional."""username_validator = UnicodeUsernameValidator()username = models.CharField(_('username'),max_length=150,unique=True,help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),validators=[username_validator],error_messages={'unique': _("A user with that username already exists."),},)first_name = models.CharField(_('first name'), max_length=150, blank=True)last_name = models.CharField(_('last name'), max_length=150, blank=True)email = models.EmailField(_('email address'), blank=True)is_staff = models.BooleanField(_('staff status'),default=False,help_text=_('Designates whether the user can log into this admin site.'),)is_active = models.BooleanField(_('active'),default=True,help_text=_('Designates whether this user should be treated as active. ''Unselect this instead of deleting accounts.'),)date_joined = models.DateTimeField(_('date joined'), default=timezone.now)objects = UserManager()EMAIL_FIELD = 'email'USERNAME_FIELD = 'username'REQUIRED_FIELDS = ['email']class Meta:verbose_name = _('user')verbose_name_plural = _('users')abstract = Truedef clean(self):super().clean()self.email = self.__class__.objects.normalize_email(self.email)def get_full_name(self):"""Return the first_name plus the last_name, with a space in between."""full_name = '%s %s' % (self.first_name, self.last_name)return full_name.strip()def get_short_name(self):"""Return the short name for the user."""return self.first_namedef email_user(self, subject, message, from_email=None, **kwargs):"""Send an email to this user."""send_mail(subject, message, from_email, [self.email], **kwargs)class AbstractBaseUser(models.Model):password = models.CharField(_('password'), max_length=128)last_login = models.DateTimeField(_('last login'), blank=True, null=True)is_active = TrueREQUIRED_FIELDS = []# Stores the raw password if set_password() is called so that it can# be passed to password_changed() after the model is saved._password = Noneclass Meta:abstract = Truedef __str__(self):return self.get_username()def save(self, *args, **kwargs):super().save(*args, **kwargs)if self._password is not None:password_validation.password_changed(self._password, self)self._password = Nonedef get_username(self):"""Return the username for this User."""return getattr(self, self.USERNAME_FIELD)def clean(self):setattr(self, self.USERNAME_FIELD, self.normalize_username(self.get_username()))def natural_key(self):return (self.get_username(),)@propertydef is_anonymous(self):"""Always return False. This is a way of comparing User objects toanonymous users."""return False@propertydef is_authenticated(self):"""Always return True. This is a way to tell if the user has beenauthenticated in templates."""return Truedef set_password(self, raw_password):self.password = make_password(raw_password)self._password = raw_passworddef check_password(self, raw_password):"""Return a boolean of whether the raw_password was correct. Handleshashing formats behind the scenes."""def setter(raw_password):self.set_password(raw_password)# Password hash upgrades shouldn't be considered password changes.self._password = Noneself.save(update_fields=["password"])return check_password(raw_password, self.password, setter)def set_unusable_password(self):# Set a value that will never be a valid hashself.password = make_password(None)def has_usable_password(self):"""Return False if set_unusable_password() has been called for this user."""return is_password_usable(self.password)def _legacy_get_session_auth_hash(self):# RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.key_salt = 'django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash'return salted_hmac(key_salt, self.password, algorithm='sha1').hexdigest()def get_session_auth_hash(self):"""Return an HMAC of the password field."""key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"return salted_hmac(key_salt,self.password,# RemovedInDjango40Warning: when the deprecation ends, replace# with:# algorithm='sha256',algorithm=settings.DEFAULT_HASHING_ALGORITHM,).hexdigest()@classmethoddef get_email_field_name(cls):try:return cls.EMAIL_FIELDexcept AttributeError:return 'email'@classmethoddef normalize_username(cls, username):return unicodedata.normalize('NFKC', username) if isinstance(username, str) else usernameclass PermissionsMixin(models.Model):"""Add the fields and methods necessary to support the Group and Permissionmodels using the ModelBackend."""is_superuser = models.BooleanField(_('superuser status'),default=False,help_text=_('Designates that this user has all permissions without ''explicitly assigning them.'),)groups = models.ManyToManyField(Group,verbose_name=_('groups'),blank=True,help_text=_('The groups this user belongs to. A user will get all permissions ''granted to each of their groups.'),related_name="user_set",related_query_name="user",)user_permissions = models.ManyToManyField(Permission,verbose_name=_('user permissions'),blank=True,help_text=_('Specific permissions for this user.'),related_name="user_set",related_query_name="user",)class Meta:abstract = Truedef get_user_permissions(self, obj=None):"""Return a list of permission strings that this user has directly.Query all available auth backends. If an object is passed in,return only permissions matching this object."""return _user_get_permissions(self, obj, 'user')def get_group_permissions(self, obj=None):"""Return a list of permission strings that this user has through theirgroups. Query all available auth backends. If an object is passed in,return only permissions matching this object."""return _user_get_permissions(self, obj, 'group')def get_all_permissions(self, obj=None):return _user_get_permissions(self, obj, 'all')def has_perm(self, perm, obj=None):"""Return True if the user has the specified permission. Query allavailable auth backends, but return immediately if any backend returnsTrue. Thus, a user who has permission from a single auth backend isassumed to have permission in general. If an object is provided, checkpermissions for that object."""# Active superusers have all permissions.if self.is_active and self.is_superuser:return True# Otherwise we need to check the backends.return _user_has_perm(self, perm, obj)def has_perms(self, perm_list, obj=None):"""Return True if the user has each of the specified permissions. Ifobject is passed, check if the user has all required perms for it."""return all(self.has_perm(perm, obj) for perm in perm_list)def has_module_perms(self, app_label):"""Return True if the user has any permissions in the given app label.Use similar logic as has_perm(), above."""# Active superusers have all permissions.if self.is_active and self.is_superuser:return Truereturn _user_has_module_perms(self, app_label)
在AbstractUser中定义了username、first_name、last_name、email、is_staff、is_active、date_joined字段,在AbstractBaseUser中定义了password、last_login字段,在PermissionsMixin中定义了is_superuser、groups 、user_permissions字段,对应起来就是Admin的用户管理界面中的内容。
先定义一个自己的用户表,修改原来的plcrm中的UserProfile:
class UserProfile(AbstractBaseUser, PermissionsMixin):email = models.EmailField(verbose_name='邮件地址',max_length=255,unique=True)name = models.CharField(max_length=32,verbose_name="账户的用户名")roles = models.ManyToManyField("Role", blank=True, verbose_name='用户对应的角色')dateofcreate = models.DateTimeField(verbose_name='创建日期')is_active = models.BooleanField(default=True)is_admin = models.BooleanField(default=False)USERNAME_FIELD = 'email' # 指定哪个字段作为登录的账户名,这里指定email作为登录的账户名REQUIRED_FIELDS = ['name'] # 指定哪些字段是必须的,即必须填写# objects = MyUserProfileManager()def __str__(self):return self.emaildef has_perm(self, perm, obj=None):"Does the user have a specific permission?"# Simplest possible answer: Yes, alwaysreturn Truedef has_module_perms(self, app_label):"Does the user have permissions to view the app `app_label`?"# Simplest possible answer: Yes, alwaysreturn True@propertydef is_staff(self):"Is the user a member of staff?"# Simplest possible answer: All admins are staffreturn self.is_adminclass Meta:verbose_name_plural ="账号表"
修改好后,删除了原来的数据库重建,然后重新生成表,如下出现错误:
这时,在项目的settings.py中增加如下配置:
AUTH_USER_MODEL = 'plcrm.UserProfile'
这一步是指定系统使用哪个模型来做用户表,默认是django.contrib.auth.models.User,要使用自己定义的,就要在配置中指定。
配置上面语句后,就可以正常makemigrations和migrate了。
成功后,创建超级用户:
提示: AttributeError: 'Manager' object has no attribute 'get_by_natural_key',这就要涉及定义用户表中的objects = xxx这一句,
设置: objects = UserProfileManager()
然后创建UserProfileManager类
class UserProfileManager(BaseUserManager):def create_user(self, email, name, password=None):"""Creates and saves a User with the given email, date ofbirth and password."""if not email:raise ValueError('Users must have an email address')user = self.model(email=self.normalize_email(email),name=name,)user.set_password(password)user.save(using=self._db)return userdef create_superuser(self, email, name, password=None):"""Creates and saves a superuser with the given email, date ofbirth and password."""user = self.create_user(email,password=password,name=name,)user.is_admin = Trueuser.save(using=self._db)return user
这个管理类提供了两个方法,create_user和create_superuser,就是用来创建普通用户和创建超级用户的,在manage.py createsuperuser,就应该调用这个类的方法。其参数是email,就是我们指定的账户名,是必须的,第二个参数是name,是我们在UserProfile中设置的必填字段,然后是password密码。
注意,这时输入的就是邮件地址,作为账户名。
此时,再登录:
进入后:
AUTHENTICATION AND AUTHORAZATION应用中已经没有Users表了,现在使用的是我们自定义的账号表(这里显示的是UserProfile的verbose_name_plural的值)。
点击这个链接Add,增加用户:
可以看到,password、邮件地址和账户的用户名是必填的,粗体提示了,有问题的是密码应该是密码输入框,这里显示明文了。并且保存后,密码是以输入的明文保存的,应该是密文 。
上面是没有Admin配置类,默认全部字段显示,进行个性化定制:
在项目的admin.py中进行配置,
# ###########以下是使用自定义UserProfile在这里必须有的一些代码####################
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from django.utils.translation import gettext, gettext_lazy as _
from plcrm.models import UserProfileclass UserProfileCreationForm(forms.ModelForm):"""A form for creating new users. Includes all the requiredfields, plus a repeated password."""password1 = forms.CharField(label='Password',widget=forms.PasswordInput)password2 = forms.CharField(label='Password confirmation',widget=forms.PasswordInput)class Meta:model = UserProfilefields = ('email','name')def clean_password2(self):# Check that the two password entries matchpassword1 = self.cleaned_data.get("password1")password2 = self.cleaned_data.get("password2")if password1 and password2 and password1 != password2:raise ValidationError("Passwords don't match")return password2def save(self, commit=True):# Save the provided password in hashed formatuser = super().save(commit=False)user.set_password(self.cleaned_data["password1"])if commit:user.save()return userclass UserProfileChangeForm(forms.ModelForm):"""A form for updating users. Includes all the fields onthe user, but replaces the password field with admin'sdisabled password hash display field."""password = ReadOnlyPasswordHashField()class Meta:model = UserProfilefields = ('email', 'password', 'name', 'is_active', 'is_admin')class UserProfileAdmin(BaseUserAdmin):# The forms to add and change user instancesform = UserProfileChangeFormadd_form = UserProfileCreationForm# The fields to be used in displaying the User model.# These override the definitions on the base UserAdmin# that reference specific fields on auth.User.list_display = ('email', 'name', 'is_admin')list_filter = ('is_admin',)fieldsets = ( # 这个字段,是点击已有的用户,显示的界面字段,可进行修改(None, {'fields': ('email', 'password')}),('Personal info', {'fields': ('name',)}),('Permissions', {'fields': ('is_admin','is_active','groups','user_permissions')}),('roles', {'fields': ('roles',)}),('Date:', {'fields': ('dateofcreate',)}),)# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin# overrides get_fieldsets to use this attribute when creating a user.add_fieldsets = ( # 这个字段是配置增加用户时,界面显示的字段(None, {'classes': ('wide',),'fields': ('email', 'name', 'password1', 'password2','dateofcreate'),}),('Roles',{'classes':('wide',),'fields':('roles',)}),)search_fields = ('email',)ordering = ('email',)filter_horizontal = ()
# ##################################### 然后注册
admin.site.register(models.UserProfile,UserProfileAdmin) # 配置上UserProfileAdmin
如上的代码后,就可以按个性化的界面进行用户管理,如创建用户,修改用户等,但是,重置用户密码没有实现,缺少了重置密码的链接,通过对Django的Admin的源代码查看,对UserProfileChangeForm进行完善:
class UserProfileChangeForm(forms.ModelForm):"""A form for updating users. Includes all the fields onthe user, but replaces the password field with admin'sdisabled password hash display field."""password = ReadOnlyPasswordHashField(label=_("Password"),help_text=_('Raw passwords are not stored, so there is no way to see this ''user’s password, but you can change the password using ''<a href="{}">重置密码</a>.' # 这个链接是进行密码重置的链接地址,需要在__init__中初始化),)class Meta:model = UserProfilefields = ('email', 'password', 'name', 'is_active', 'is_admin')def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)password = self.fields.get('password')if password:password.help_text = password.help_text.format('../password/') # 这是对密码给出的帮助中的链接的赋值user_permissions = self.fields.get('user_permissions')if user_permissions:user_permissions.queryset = user_permissions.queryset.select_related('content_type')
在ReadOnlyPasswordHashField这个函数中增加help_text,其中有一个链接,在__init__中对这个链接进行赋值,可以转到重置密码页面,进行密码的重置。这里因为是继承过来的,__init__()函数可以不写,这里主要是给出其实现的细节,弄清楚原理。
这样,我们就可以对UserProfile进行个性化的扩展,需要什么字段,就添加什么字段,一般是角色、组、机构等一起配置。