|
1 | 1 | from django.contrib.admin import ModelAdmin, StackedInline |
| 2 | +from django.core.exceptions import FieldError |
2 | 3 | from django.urls import reverse |
3 | 4 | from django.utils.translation import gettext_lazy as _ |
4 | 5 |
|
@@ -69,33 +70,86 @@ def has_changed(self): |
69 | 70 | return super().has_changed() |
70 | 71 |
|
71 | 72 |
|
72 | | -class UUIDAdmin(ModelAdmin): |
| 73 | +class CopyableFieldError(FieldError): |
| 74 | + pass |
| 75 | + |
| 76 | + |
| 77 | +class CopyableFieldsAdmin(ModelAdmin): |
73 | 78 | """ |
74 | | - Defines a field name uuid whose value is that |
75 | | - of the id of the object |
| 79 | + An admin class that allows to set admin |
| 80 | + fields to be read-only and makes it easy |
| 81 | + to copy the fields contents. |
| 82 | + Useful for auto-generated fields such as |
| 83 | + UUIDs, secret keys, tokens, etc |
76 | 84 | """ |
77 | 85 |
|
78 | | - def uuid(self, obj): |
79 | | - return obj.pk |
80 | | - |
81 | | - def _process_fields(self, fields, request, obj): |
82 | | - fields = list(fields) |
83 | | - if 'uuid' in fields and not obj: |
84 | | - fields.remove('uuid') |
85 | | - if 'uuid' not in fields and obj: |
86 | | - fields.insert(0, 'uuid') |
87 | | - return tuple(fields) |
| 86 | + copyable_fields = () |
| 87 | + change_form_template = 'admin/change_form.html' |
| 88 | + |
| 89 | + def _check_copyable_subset_fields(self, copyable_fields, fields): |
| 90 | + if not set(copyable_fields).issubset(fields): |
| 91 | + class_name = self.__class__.__name__ |
| 92 | + raise CopyableFieldError( |
| 93 | + ( |
| 94 | + f'{copyable_fields} not in {class_name}.fields {fields}, ' |
| 95 | + f'Check copyable_fields attribute of class {class_name}.' |
| 96 | + ) |
| 97 | + ) |
88 | 98 |
|
89 | 99 | def get_fields(self, request, obj=None): |
90 | | - fields = super().get_fields(request, obj) |
91 | | - return self._process_fields(fields, request, obj) |
| 100 | + fields = super(ModelAdmin, self).get_fields(request, obj) |
| 101 | + self._check_copyable_subset_fields(self.copyable_fields, fields) |
| 102 | + # We should exclude `copyable_fields` fields |
| 103 | + # when the object doesn't exist for example, |
| 104 | + # in the case of `add_view` because `copyable_fields` |
| 105 | + # can't be edited and are auto generated by the system |
| 106 | + if not obj: |
| 107 | + return tuple(set(fields).difference(self.copyable_fields)) |
| 108 | + return fields |
92 | 109 |
|
93 | 110 | def get_readonly_fields(self, request, obj=None): |
94 | | - fields = super().get_readonly_fields(request, obj) |
95 | | - return self._process_fields(fields, request, obj) |
| 111 | + readonly_fields = super(ModelAdmin, self).get_readonly_fields(request, obj) |
| 112 | + if not obj: |
| 113 | + return readonly_fields |
| 114 | + # Make sure `copyable_fields` is included in `read_only` fields |
| 115 | + return tuple([*readonly_fields, *self.copyable_fields]) |
| 116 | + |
| 117 | + def add_view(self, request, form_url='', extra_context=None): |
| 118 | + extra_context = extra_context or {} |
| 119 | + extra_context['copyable_fields'] = [] |
| 120 | + return super().add_view( |
| 121 | + request, |
| 122 | + form_url, |
| 123 | + extra_context=extra_context, |
| 124 | + ) |
| 125 | + |
| 126 | + def change_view(self, request, object_id, form_url='', extra_context=None): |
| 127 | + extra_context = extra_context or {} |
| 128 | + extra_context['copyable_fields'] = list(self.copyable_fields) |
| 129 | + return super().change_view( |
| 130 | + request, |
| 131 | + object_id, |
| 132 | + form_url, |
| 133 | + extra_context=extra_context, |
| 134 | + ) |
96 | 135 |
|
97 | 136 | class Media: |
98 | | - js = ('admin/js/jquery.init.js', 'openwisp-utils/js/uuid.js') |
| 137 | + js = ('admin/js/jquery.init.js', 'openwisp-utils/js/copyable.js') |
| 138 | + |
| 139 | + |
| 140 | +class UUIDAdmin(CopyableFieldsAdmin): |
| 141 | + """ |
| 142 | + This class is a subclass of `CopyableFieldsAdmin` |
| 143 | + which sets `uuid` as the only copyable field |
| 144 | + This class is kept for backward compatibility |
| 145 | + and convenience, since different models of various |
| 146 | + OpenWISP modules show `uuid` as the only copyable field |
| 147 | + """ |
| 148 | + |
| 149 | + copyable_fields = ('uuid',) |
| 150 | + |
| 151 | + def uuid(self, obj): |
| 152 | + return obj.pk |
99 | 153 |
|
100 | 154 | uuid.short_description = _('UUID') |
101 | 155 |
|
|
0 commit comments