admin.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. from django import forms
  2. from django.contrib import admin
  3. from django.contrib.auth.admin import UserAdmin as UserAdmin_
  4. from django.contrib.auth.models import Group, User
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.core.exceptions import FieldError, ValidationError
  7. from extras.admin import order_content_types
  8. from .models import AdminGroup, AdminUser, ObjectPermission, Token, UserConfig
  9. #
  10. # Inline models
  11. #
  12. class ObjectPermissionInline(admin.TabularInline):
  13. exclude = None
  14. extra = 3
  15. readonly_fields = ['object_types', 'actions', 'constraints']
  16. verbose_name = 'Permission'
  17. verbose_name_plural = 'Permissions'
  18. def get_queryset(self, request):
  19. return super().get_queryset(request).prefetch_related('objectpermission__object_types')
  20. @staticmethod
  21. def object_types(instance):
  22. # Don't call .values_list() here because we want to reference the pre-fetched object_types
  23. return ', '.join([ot.name for ot in instance.objectpermission.object_types.all()])
  24. @staticmethod
  25. def actions(instance):
  26. return ', '.join(instance.objectpermission.actions)
  27. @staticmethod
  28. def constraints(instance):
  29. return instance.objectpermission.constraints
  30. class GroupObjectPermissionInline(ObjectPermissionInline):
  31. model = AdminGroup.object_permissions.through
  32. class UserObjectPermissionInline(ObjectPermissionInline):
  33. model = AdminUser.object_permissions.through
  34. class UserConfigInline(admin.TabularInline):
  35. model = UserConfig
  36. readonly_fields = ('data',)
  37. can_delete = False
  38. verbose_name = 'Preferences'
  39. #
  40. # Users & groups
  41. #
  42. # Unregister the built-in GroupAdmin and UserAdmin classes so that we can use our custom admin classes below
  43. admin.site.unregister(Group)
  44. admin.site.unregister(User)
  45. @admin.register(AdminGroup)
  46. class GroupAdmin(admin.ModelAdmin):
  47. fields = ('name',)
  48. list_display = ('name', 'user_count')
  49. ordering = ('name',)
  50. search_fields = ('name',)
  51. inlines = [GroupObjectPermissionInline]
  52. @staticmethod
  53. def user_count(obj):
  54. return obj.user_set.count()
  55. @admin.register(AdminUser)
  56. class UserAdmin(UserAdmin_):
  57. list_display = [
  58. 'username', 'email', 'first_name', 'last_name', 'is_superuser', 'is_staff', 'is_active'
  59. ]
  60. fieldsets = (
  61. (None, {'fields': ('username', 'password', 'first_name', 'last_name', 'email')}),
  62. ('Groups', {'fields': ('groups',)}),
  63. ('Status', {
  64. 'fields': ('is_active', 'is_staff', 'is_superuser'),
  65. }),
  66. ('Important dates', {'fields': ('last_login', 'date_joined')}),
  67. )
  68. filter_horizontal = ('groups',)
  69. def get_inlines(self, request, obj):
  70. if obj is not None:
  71. return (UserObjectPermissionInline, UserConfigInline)
  72. return ()
  73. #
  74. # REST API tokens
  75. #
  76. class TokenAdminForm(forms.ModelForm):
  77. key = forms.CharField(
  78. required=False,
  79. help_text="If no key is provided, one will be generated automatically."
  80. )
  81. class Meta:
  82. fields = [
  83. 'user', 'key', 'write_enabled', 'expires', 'description'
  84. ]
  85. model = Token
  86. @admin.register(Token)
  87. class TokenAdmin(admin.ModelAdmin):
  88. form = TokenAdminForm
  89. list_display = [
  90. 'key', 'user', 'created', 'expires', 'write_enabled', 'description'
  91. ]
  92. #
  93. # Permissions
  94. #
  95. class ObjectPermissionForm(forms.ModelForm):
  96. can_view = forms.BooleanField(required=False)
  97. can_add = forms.BooleanField(required=False)
  98. can_change = forms.BooleanField(required=False)
  99. can_delete = forms.BooleanField(required=False)
  100. class Meta:
  101. model = ObjectPermission
  102. exclude = []
  103. help_texts = {
  104. 'actions': 'Actions granted in addition to those listed above',
  105. 'constraints': 'JSON expression of a queryset filter that will return only permitted objects. Leave null '
  106. 'to match all objects of this type.'
  107. }
  108. labels = {
  109. 'actions': 'Additional actions'
  110. }
  111. widgets = {
  112. 'constraints': forms.Textarea(attrs={'class': 'vLargeTextField'})
  113. }
  114. def __init__(self, *args, **kwargs):
  115. super().__init__(*args, **kwargs)
  116. # Make the actions field optional since the admin form uses it only for non-CRUD actions
  117. self.fields['actions'].required = False
  118. # Format ContentType choices
  119. order_content_types(self.fields['object_types'])
  120. self.fields['object_types'].choices.insert(0, ('', '---------'))
  121. # Order group and user fields
  122. self.fields['groups'].queryset = self.fields['groups'].queryset.order_by('name')
  123. self.fields['users'].queryset = self.fields['users'].queryset.order_by('username')
  124. # Check the appropriate checkboxes when editing an existing ObjectPermission
  125. if self.instance.pk:
  126. for action in ['view', 'add', 'change', 'delete']:
  127. if action in self.instance.actions:
  128. self.fields[f'can_{action}'].initial = True
  129. self.instance.actions.remove(action)
  130. def clean(self):
  131. object_types = self.cleaned_data['object_types']
  132. constraints = self.cleaned_data['constraints']
  133. # Append any of the selected CRUD checkboxes to the actions list
  134. if not self.cleaned_data.get('actions'):
  135. self.cleaned_data['actions'] = list()
  136. for action in ['view', 'add', 'change', 'delete']:
  137. if self.cleaned_data[f'can_{action}'] and action not in self.cleaned_data['actions']:
  138. self.cleaned_data['actions'].append(action)
  139. # At least one action must be specified
  140. if not self.cleaned_data['actions']:
  141. raise ValidationError("At least one action must be selected.")
  142. # Validate the specified model constraints by attempting to execute a query. We don't care whether the query
  143. # returns anything; we just want to make sure the specified constraints are valid.
  144. if constraints:
  145. for ct in object_types:
  146. model = ct.model_class()
  147. try:
  148. model.objects.filter(**constraints).exists()
  149. except FieldError as e:
  150. raise ValidationError({
  151. 'constraints': f'Invalid filter for {model}: {e}'
  152. })
  153. class ActionListFilter(admin.SimpleListFilter):
  154. title = 'action'
  155. parameter_name = 'action'
  156. def lookups(self, request, model_admin):
  157. options = set()
  158. for action_list in ObjectPermission.objects.values_list('actions', flat=True).distinct():
  159. options.update(action_list)
  160. return [
  161. (action, action) for action in sorted(options)
  162. ]
  163. def queryset(self, request, queryset):
  164. if self.value():
  165. return queryset.filter(actions=[self.value()])
  166. class ObjectTypeListFilter(admin.SimpleListFilter):
  167. title = 'object type'
  168. parameter_name = 'object_type'
  169. def lookups(self, request, model_admin):
  170. object_types = ObjectPermission.objects.values_list('id', flat=True).distinct()
  171. content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
  172. return [
  173. (ct.pk, ct) for ct in content_types
  174. ]
  175. def queryset(self, request, queryset):
  176. if self.value():
  177. return queryset.filter(object_types=self.value())
  178. @admin.register(ObjectPermission)
  179. class ObjectPermissionAdmin(admin.ModelAdmin):
  180. actions = ('enable', 'disable')
  181. fieldsets = (
  182. (None, {
  183. 'fields': ('name', 'description', 'enabled')
  184. }),
  185. ('Actions', {
  186. 'fields': (('can_view', 'can_add', 'can_change', 'can_delete'), 'actions')
  187. }),
  188. ('Objects', {
  189. 'fields': ('object_types',)
  190. }),
  191. ('Assignment', {
  192. 'fields': ('groups', 'users')
  193. }),
  194. ('Constraints', {
  195. 'fields': ('constraints',),
  196. 'classes': ('monospace',)
  197. }),
  198. )
  199. filter_horizontal = ('object_types', 'groups', 'users')
  200. form = ObjectPermissionForm
  201. list_display = [
  202. 'name', 'enabled', 'list_models', 'list_users', 'list_groups', 'actions', 'constraints', 'description',
  203. ]
  204. list_filter = [
  205. 'enabled', ActionListFilter, ObjectTypeListFilter, 'groups', 'users'
  206. ]
  207. search_fields = ['actions', 'constraints', 'description', 'name']
  208. def get_queryset(self, request):
  209. return super().get_queryset(request).prefetch_related('object_types', 'users', 'groups')
  210. def list_models(self, obj):
  211. return ', '.join([f"{ct}" for ct in obj.object_types.all()])
  212. list_models.short_description = 'Models'
  213. def list_users(self, obj):
  214. return ', '.join([u.username for u in obj.users.all()])
  215. list_users.short_description = 'Users'
  216. def list_groups(self, obj):
  217. return ', '.join([g.name for g in obj.groups.all()])
  218. list_groups.short_description = 'Groups'
  219. #
  220. # Admin actions
  221. #
  222. def enable(self, request, queryset):
  223. updated = queryset.update(enabled=True)
  224. self.message_user(request, f"Enabled {updated} permissions")
  225. def disable(self, request, queryset):
  226. updated = queryset.update(enabled=False)
  227. self.message_user(request, f"Disabled {updated} permissions")