Django自定义admin界面 - Part2

2009-11-18 13:49

接着昨天的内容写 :D

为不同的用户提供不同的域集(Fieldset)

这个不算是很复杂的hack, 在ModelAdmin里面定义一个域集, 然后用自定义的get_fieldsets方法override掉默认的方法即可:

class ProjectAdmin(admin.ModelAdmin):
selected_fieldsets = (
(None, {'fields': ('fullname', 'shortname', 'popo_acount',
'sms_time', 'maintain_time', 'sa', 'programmer', 'rules')}),
)

def get_fieldsets(self, request, obj=None):
"""
        This function would limit the fieldset for project admins.
        For project admins, we used selected_fieldsets.
        """
if request.user.is_superuser:
# Show me everything, for I'm root.
return super(ProjectAdmin, self).get_fieldsets(request, obj)
elif request.user.get_profile().is_admin():
# Show project admins selected fields.
return self.selected_fieldsets

在上面的例子里面, 我们用request里面的用户信息来对用户加以区分, 如果是超级用户, 那么调用父类的默认方法, 不要override, 而当用户是项目管理员时, 直接返回选择过的, 没有关键信息的域集(Fieldset). 这样能够限制用户对关键信息的访问和修改.

权限控制

Django对于模型有具体的权限控制, 例如, 你可以规定这个用户有添加, 修改或删除User模型的权限. 但是当需要根据管理员身份而对这个权限加以细分时, 这个权限控制就不够了, 我们需要自定义has_add_permission, has_change_permission等方法. 在我的项目中, 我自定义了后者:

def has_change_permission(self, request, obj=None):
"""
        Returns True if the given request has permission to change the given
        Django model instance.

        If `obj` is None, this should return True if the given request has
        permission to change *any* object of the given type.
        """
if obj:
project = obj
user = request.user
if user.is_superuser:
return True
else:
project_list = request.user.get_profile().project_set.all()
if project in project_list:
return True
else:
return False

这个依然不复杂, 核心思想仍然是根据request中的用户信息而对返回的内容加以控制.

自定义保存方法

save_model是一个很多人都会修改的方法, 自定义保存方法能够让你在保存时做一些额外的事情. 比如在IBM DevWork上那篇文章就用save_model方法来保存了request里面的用户信息. 好吧, DevWork上这篇文章并不是一个特别好的例子, 因为这种事情最好在视图函数里面搞定, 再不济也该写一个信号自动完成, 放在这儿是不恰当的. 而且这篇文章里面给的例子相对较简单, 我要做的事情会更复杂一点. 我需要自定义的是, 当admin界面下模型的修改提交后, 我需要根据用户权限来对m2m关系的变化加以限制. 因此, 我还需要修改form默认的save_m2m方法, 废话少说, 放代码先~

def save_model(self, request, instance, form, change):
"""
        Like the save_model in MyUserAdmin, we need to setup the value of an
        m2m field in this function.
        """
def my_save_m2m():
# We have two m2m fields in Project model, the
# 'admins' field and the 'rules' field.
for field in instance._meta.many_to_many:
if field.name == 'admins':
# We just don't save it, meaning this field cannot be
# changed by this user.
pass
else:
cleaned_data = form.cleaned_data
field.save_form_data(instance, cleaned_data[field.name])

instance.save()
user = request.user
if user.get_profile().is_admin() and not user.is_superuser:
form.save_m2m = my_save_m2m

诸位看官先表看那个my_save_m2m方法, 先看后面的. 首先老老实实地用instance.save()来保存这个模型里面的非m2m关系. 然后根据用户的类型来对用户执行的save_m2m方法加以修改. 逻辑判断不细说了, 只是当用户满足某个条件时, 则form的save_m2m方法就会使用我们上面定义的my_save_m2m方法.

再来回头看我定义的my_save_m2m方法, 因为我所要保存的这个模型有两m2m关系, 因此我需要根据修改的具体的域的名字来加以区分, 这个信息在instance._meta.many_to_many里面有, 具体判断的方法参见代码了. 注意到第13行用的pass表示不对这个域的m2m关系加以保存. 而后面第15, 16两行的代码表示按照默认的方法保存(实际上这个是抄默认的m2m关系的, 嘿嘿~).

自定义MultiSelect中出现的值

好吧, 这是一个很特殊的需求. 我需要根据不同的用户而在一个m2m的MultiSelect控件中显示不同的值. 对于超级用户, 我希望他/她能看到所有可能的值, 对于有项目管理员(具有部分权限的用户), 我希望他/她只能看到和自己项目相关的内容.

这一个需求在参考资料2中的那个Slide的第50页有一个类似的实现, 不过这个实现相对更简单, 因为它没有根据用户的不同而加以处理, 而仅仅是根据指定的值来修改控件中的值. 事实上, 如果不做进一步的修正, 在ModelForm中也不可能根据用户的不同而筛选出不同的值, 因为逻辑上一个ModelForm里面不应该有请求相关的信息, 于是也就不能做这种筛选了. 于是, 为了实现我的需求, 我强制的将request作为参数而在实例化这个ModelForm的时候传给了form, 为此, 我需要override默认的change_view方法:

form = MyUserprofileForm
def change_view(self, request, object_id, extra_context=None):
""" some code here. """
if request.method == 'POST':
""" more code here. """
else:
form = ModelForm(instance=obj, request = request)
""" even more code here. """

上面这几行代码只是给出了change_view这个很长的函数的梗概, 我所修改的是第7行. 将一个request作为参数传给了ModelForm. 然后, 我们需要定义自己的ModelForm:

class MyUserprofileForm(forms.ModelForm):
"""
    To show only those projects that can be managed by the project admin, 
    we have to setup our form manually.
    """
def __init__(self, *args, **kwargs):
request = kwargs.get('request', None)
if request:
del kwargs['request']
super(MyUserprofileForm, self).__init__(*args, **kwargs)
if request:
userprofile = request.user.get_profile()
# Set project widget
project_widget = self.fields['projects'].widget
if userprofile.user.is_superuser:
projects = Project.objects.all()
elif userprofile.is_admin():
projects = userprofile.project_set.all()
choices = []
for choice in projects:
choices.append((choice.id, choice.fullname))
project_widget.choices = choices

class Meta:
model = Userprofile

在__init__函数的一开始, 我们就试图从kwargs中拿到request变量. 然后将request从kwargs中删除后直接调用原有的__init__方法来完成正常的初始化过程. 如果kwargs中有request, 我们就需要根据request中的用户信息来对project对应的控件加以修改. 这一段代码可读性比较好, 而且比较死, 因此就不再多解释了~