Skip to content

Commit f7af154

Browse files
committed
[feature] Added CopyableFieldsAdmin which generalize UUIDAdmin class #328
Closes #328
1 parent 1342f13 commit f7af154

File tree

4 files changed

+112
-6
lines changed

4 files changed

+112
-6
lines changed

openwisp_utils/admin.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.admin import ModelAdmin, StackedInline
2+
from django.core.exceptions import FieldError
23
from django.urls import reverse
34
from django.utils.translation import gettext_lazy as _
45

@@ -69,10 +70,10 @@ def has_changed(self):
6970
return super().has_changed()
7071

7172

72-
class UUIDAdmin(ModelAdmin):
73+
class UUIDAdmin(ModelAdmin): # pragma: no cover
7374
"""
7475
Defines a field name uuid whose value is that
75-
of the id of the object
76+
of the id of the object (deprecated)
7677
"""
7778

7879
def uuid(self, obj):
@@ -100,6 +101,77 @@ class Media:
100101
uuid.short_description = _('UUID')
101102

102103

104+
class CopyableFieldError(FieldError):
105+
pass
106+
107+
108+
class CopyableFieldsAdmin(ModelAdmin):
109+
"""
110+
An admin class that allows us to set read-only input fields
111+
which makes admin fields easier and quicker to copy and paste.
112+
"""
113+
114+
copyable_fields = ()
115+
change_form_template = 'admin/change_form.html'
116+
117+
def uuid(self, obj):
118+
return obj.pk
119+
120+
def _check_copyable_subset_fields(self, copyable_fields, fields):
121+
if not set(copyable_fields).issubset(fields):
122+
class_name = self.__class__.__name__
123+
raise CopyableFieldError(
124+
(
125+
f'{copyable_fields} not in {class_name}.fields {fields}, '
126+
f'Check copyable_fields attribute of class {class_name}.'
127+
)
128+
)
129+
130+
def _process_copyable_fields(self, fields, request, obj):
131+
fields = list(fields)
132+
copyable_fields = list(self.copyable_fields)
133+
self._check_copyable_subset_fields(copyable_fields, fields)
134+
# if `uuid`` is in `copyable_fields` and the
135+
# instance doesn't exist (i.e. in the case of "add_view")
136+
# then we need to exclude it from model admin fields
137+
if 'uuid' in copyable_fields and not obj:
138+
fields.remove('uuid')
139+
return tuple(fields)
140+
141+
def get_fields(self, request, obj=None):
142+
fields = super().get_fields(request, obj)
143+
return self._process_copyable_fields(fields, request, obj)
144+
145+
def get_readonly_fields(self, request, obj=None):
146+
fields = super().get_readonly_fields(request, obj)
147+
# model instance doesn't exist (i.e. in the case of "add_view")
148+
if not obj:
149+
return fields
150+
# make sure `copyable_fields` is included `read_only` fields
151+
return tuple([*fields, *self.copyable_fields])
152+
153+
def add_view(self, request, form_url='', extra_context=None):
154+
extra_context = extra_context or {}
155+
extra_context['copyable_fields'] = []
156+
return super().add_view(
157+
request,
158+
form_url,
159+
extra_context=extra_context,
160+
)
161+
162+
def change_view(self, request, object_id, form_url='', extra_context=None):
163+
extra_context = extra_context or {}
164+
extra_context['copyable_fields'] = list(self.copyable_fields)
165+
return super().change_view(
166+
request,
167+
object_id,
168+
form_url,
169+
extra_context=extra_context,
170+
)
171+
172+
uuid.short_description = _('UUID')
173+
174+
103175
class ReceiveUrlAdmin(ModelAdmin):
104176
"""
105177
Return a receive_url field whose value is that of
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{% extends "admin/change_form.html" %}
2+
{% block admin_change_form_document_ready %}
3+
{{ block.super }}
4+
<script>
5+
(function ($) {
6+
$(document).ready(function () {
7+
var copyableFields = {{ copyable_fields | safe }} || [];
8+
9+
copyableFields.forEach(copyableField => {
10+
11+
var copyableFieldContainer = $(`.field-${copyableField} .readonly`).eq(0);
12+
13+
copyableFieldContainer.html(`<input readonly id="id_${copyableField}" type="text"
14+
class="vTextField readonly" value="${copyableFieldContainer.text()}">`);
15+
16+
var copyableFieldSelectedId = $(`#id_${copyableField}`);
17+
copyableFieldSelectedId.click(function () {
18+
$(this).select();
19+
});
20+
})
21+
})
22+
}) (django.jQuery);
23+
</script>
24+
{% endblock %}

tests/test_project/admin.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from django.utils.translation import gettext_lazy as _
55
from openwisp_utils.admin import (
66
AlwaysHasChangedMixin,
7+
CopyableFieldsAdmin,
78
HelpTextStackedInline,
89
ReadOnlyAdmin,
910
ReceiveUrlAdmin,
1011
TimeReadonlyAdminMixin,
11-
UUIDAdmin,
1212
)
1313
from openwisp_utils.admin_theme.filters import (
1414
AutocompleteFilter,
@@ -73,12 +73,13 @@ class OperatorInline(HelpTextStackedInline):
7373

7474

7575
@admin.register(Project)
76-
class ProjectAdmin(UUIDAdmin, ReceiveUrlAdmin):
76+
class ProjectAdmin(CopyableFieldsAdmin, ReceiveUrlAdmin):
7777
inlines = [OperatorInline]
7878
list_display = ('name',)
7979
fields = ('uuid', 'name', 'key', 'receive_url')
80-
readonly_fields = ('uuid', 'receive_url')
80+
readonly_fields = ('receive_url',)
8181
receive_url_name = 'receive_project'
82+
copyable_fields = ('uuid',)
8283

8384

8485
class ShelfFilter(SimpleInputFilter):

tests/test_project/tests/test_admin.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django.core.exceptions import ImproperlyConfigured
77
from django.test import TestCase
88
from django.urls import reverse
9-
from openwisp_utils.admin import ReadOnlyAdmin
9+
from openwisp_utils.admin import CopyableFieldError, ReadOnlyAdmin
1010
from openwisp_utils.admin_theme import settings as admin_theme_settings
1111
from openwisp_utils.admin_theme.apps import OpenWispAdminThemeConfig, _staticfy
1212
from openwisp_utils.admin_theme.checks import admin_theme_settings_checks
@@ -204,6 +204,15 @@ def test_uuid_field_in_add(self):
204204
self.assertNotContains(response, 'field-uuid')
205205
self.assertContains(response, 'field-receive_url')
206206

207+
def test_invalid_copyable_field_error(self):
208+
project = Project.objects.create(name='test_invalid_copyable_field_error')
209+
ma = ProjectAdmin(Project, self.site)
210+
copyable_field_err = "['invalid_field'] not in ProjectAdmin.fields"
211+
ma.copyable_fields = ('invalid_field',)
212+
with self.assertRaises(CopyableFieldError) as err:
213+
ma.get_fields(self.client.request, project)
214+
self.assertIn(copyable_field_err, err.exception.args[0])
215+
207216
def test_receive_url_admin(self):
208217
p = Project.objects.create(name='test_receive_url_admin_project')
209218
ma = ProjectAdmin(Project, self.site)

0 commit comments

Comments
 (0)