Как то раз у меня возникла задача добавить возможность комплексного внесения данных в БД, для этого требуется заполнить несколько полей относящихся к разным объектам, но добавить ее нужно в одной транзакции. Конечно можно написать такой код для фронтенда, но, а если нужно именно в “админке”.
Первый вариант предлагал просто добавить недостающие поле в наследник AdminModel, однако с этим методом, который работал в старых версиях до 3.х возникли проблемы, так как валидация Django находила факт отсутствия поля в модели данных.
На самом деле под формой я подразумеваю, комплекс из контроллеров (url) и отображений, решающий задачи просмотре и редактирования данных. Для моей задачи нужна форма для добавления данных.
Долго поискав в интернете я нашел несколько вариантов решения проблемы:
Вариант 1 создание формы на основе “виртуальной” моделе данных.
Метод был найден в репозитории “idlesign/django-etc” на гите суть его заключается в создании абстрактной модели данных в которой описываются необходимые поля, а потом специализированным класом наследником AdminModel, регистрируется в admin.site:
Код здесь привожу с небольшими изменениями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
class EtcAdmin(admin.ModelAdmin): """Base etc admin.""" def message_success(self, request: HttpRequest, msg: str): self.message_user(request, msg, messages.SUCCESS) def message_warning(self, request: HttpRequest, msg: str): self.message_user(request, msg, messages.WARNING) def message_error(self, request: HttpRequest, msg: str): self.message_user(request, msg, messages.ERROR) class ReadonlyAdmin(EtcAdmin): """Read-only etc admin base class.""" view_on_site: bool = False actions = None def has_add_permission(self, request: HttpRequest) -> bool: return False def has_delete_permission(self, request: HttpRequest, obj: models.Model = None) -> bool: return False def has_change_permission(self, request, obj=None): return False def changeform_view( self, request: HttpRequest, object_id: int = None, form_url: str = '', extra_context: dict = None ) -> HttpResponse: extra_context = extra_context or {} extra_context.update({ 'show_save_and_continue': False, 'show_save': False, }) return super().changeform_view(request, object_id, extra_context=extra_context) class CustomPageModelAdmin(ReadonlyAdmin): """Base for admin pages with contents based on custom models.""" def get_urls(self) -> list: meta = self.model._meta patterns = [path( '', self.admin_site.admin_view(self.view_custom), name=f'{meta.app_label}_{meta.model_name}_changelist' )] return patterns def has_add_permission(self, request: HttpRequest) -> bool: return True def view_custom(self, request: HttpRequest) -> HttpResponse: context: dict = { 'show_save_and_continue': False, 'show_save_and_add_another': False, 'title': self.model._meta.verbose_name, } return self._changeform_view(request, object_id=None, form_url='', extra_context=context) def response_add(self, request: HttpRequest, obj: 'CustomModelPage', post_url_continue=None): return HttpResponseRedirect(request.path) def save_model(self, request: HttpRequest, obj: 'CustomModelPage', form, change): obj.bound_request = request obj.bound_admin = self obj.save() class CustomModelPage(models.Model): """Allows construction of admin pages based on user input. Define your fields (as usual in models) and override .save() method. .. code-block:: python class MyPage(CustomModelPage): title = 'Test page 1' # set page title # Define some fields. my_field = models.CharField('some title', max_length=10) def save(self): ... # Implement data handling. super().save() # Register my page within Django admin. MyPage.register() """ title: str = _('Custom page') """Page title to be used.""" app_label: str = 'admin' """Application label to relate page to. Default: admin""" bound_request: Optional[HttpRequest] = None """Request object bound to the model""" bound_admin: Optional[EtcAdmin] = None """Django admin model bound to this model.""" class Meta: abstract = True managed = False @classmethod def __init_subclass__(cls) -> None: meta = cls.Meta meta.verbose_name = meta.verbose_name_plural = cls.title meta.app_label = cls.app_label super().__init_subclass__() @classmethod def register(cls, *, admin_model: CustomPageModelAdmin = None): """Registers this model page class in Django admin. :param admin_model: """ register(cls)(admin_model or CustomPageModelAdmin) def save(self): # noqa """Heirs should implement their own save handling.""" self.bound_admin.message_success(self.bound_request, _('Done.')) |
Класс EtcAdmin, это сущность позволяющая выводить сообщения в админ интерфейс (так как форма замкнута сама на себя, то требуется уведомить пользователя об успешности операции). Класс ReadonlyAdmin создает форму доступную только для просмотра данных (все метода редактирования заблокированы), вполне может пригодится и в других проектах ;). Самое интересное сосредоточено в двух последних класах. Класс CustomPageModelAdmin – это класс админ формы для добавления данных, он создает метод view_custom, который регистрирует как контроллер для редактирования и переопределяет метод save_model, отвечающий за сохранения модели, делегируя сохранения модели. И последний класс CustomModelPage,наследники которого и будут описывать форму с требуемыми полями, а все операции по обработки данных осуществляются в методе save.
Огромный плюс этого метода заключается в том, что он позволяет использовать всю мощь админ интерфейса. Единственный минус, это невозможность зарегистрировать форму через admin.site.register. Регистрация всегда осуществляется через вызов register класса CustomModelPage или его наследника.
Например, пускай есть некая модель в базе Model1 и необходима форма, тогда возможна даже такая функциональность:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class StandartModel1Admin(admin.ModelAdmin): ordering = ['some_feild_name'] search_fields = ['some_feild_name'] class SomeCustomAbsractModel(CustomModelPage): title = 'Заголовок' # set page title # Define some fields. field_to_model_1 = models.ForeignKey(Model1, on_delete=models.CASCADE, verbose_name='description') comment = models.TextField(verbose_name='Коментарий') def clean(self): if not hasattr(self, 'field_to_model_1'): raise ValidationError(_('Error')) super().clean() def save(self): if not hasattr(self, 'expense_type'): raise ValidationError(_('Тип затраты обязателен')) # ----- do it usfull -------- class SomeCustomAdminForm(CustomPageModelAdmin): autocomplete_fields = ('field_to_model_1') SomeCustomAbsractModel.register(admin_model=SomeCustomAbsractModel) |
Тут в примере StandartModel1Admin это стандартная функциональность администрирования модели, в SomeCustomAdminForm(CustomPageModelAdmin):которой определены поля для поиска и сортировки. SomeCustomAbsractModel – это наша специальная форма в которой есть необходимые поля, вализация полей производится в методе clean, а сохранение в save. Класс SomeCustomAdminForm это наследник CustomPageModelAdmin, в котором используется стандартная настройка интерфейса администрирования, например определена возможность autocomplete_fields, поиск для моделей ManyToOne и ManyToMany.
Метод 2. Переопределения методов get_fieldsets или get_fields
Этот метод взят с сайта stackoverflow.com и вот тут и немного доработан
Например создав такой миксин можно добавлять поля в формы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class AddDynamicFieldMixin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None): fs = super().get_fieldsets(request, obj) new_dynamic_fieldsets = getattr(self, 'dynamic_fieldsets', {}) for set_name, field_def_list in new_dynamic_fieldsets.items(): for field_name, field_def in field_def_list: # `gf.append(field_name)` results in multiple instances of the new fields fs = fs + ((set_name, {'fields': (field_name,)}),) # updating base_fields seems to have the same effect self.form.declared_fields.update({field_name: field_def}) return fs def get_fields(self, request, obj=None): gf = super().get_fields(request, obj) new_dynamic_fields = getattr(self, 'dynamic_fields', []) # without updating get_fields, the admin form will display w/o any new fields # without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin. for field_name, field_def in new_dynamic_fields: # `gf.append(field_name)` results in multiple instances of the new fields gf = gf + [field_name] # updating base_fields seems to have the same effect self.form.declared_fields.update({field_name: field_def}) return gf |
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class CarAdmin(admin.ModelAdmin): fieldsets = ( ('Investor', {'fields': ('car_investor',)}), ('Car Model', {'fields': ('model',)}), ('Car Info', {'fields': ( 'name', 'year', 'mileage_at_start', 'investment', 'date_start', 'control_mileage', 'last_TO_date')}) ) readonly_fields = ('investment', 'date_start', 'control_mileage', 'last_TO_date') autocomplete_fields = ('car_investor', 'model') dynamic_fieldsets = { 'Инвестиции': [('start_amount', forms.IntegerField(label='Стоимость инвестиции', min_value=0))], } def save_form(self, request, form, change): return form.save(commit=False) def save_model(self, request, obj, form, change): # obj=CarCreator.add_new_car_from_id( # model_id=form.data['model'], # investor_id=form.data['car_investor'], # car_plate=form.data['name'], # year=int(form.data['year']), # mileage_at_start=int(form.data['mileage_at_start']), # start_amount=int(form.data['start_amount']) # ) obj.control_mileage = int(form.data['mileage_at_start']) obj.investment = InvestmentCarBalance(name=obj.name, create_date=now().date(), currency=Account.AccountCurrency.DOLLAR) obj.investment.save() super().save_model(request, obj, form, change) obj.save() transaction = Balance.form_transaction(Balance.INVESTMENT, [(obj.investment, None, int(form.data['start_amount']))]) transaction.save() |
Вся сложность данного метода в требовании достаточно сложного определения dynamic_fieldsets или dynamic_fields. Вся полезная работа делается в методе save_model.
Метод 3. Добавлене полей только для чтения
Этот метод подойдет для случаев когда просто в таблицу для вывода требуется добавить какие то данные для отображения. Я нашел его на сайте xxx-cook-book.gitbooks.io.
Это довольно простой метод который требует определения некоторого метода для формирования отображения и внесения его в список полей только для чтения (readonly_fields)
1 2 3 4 5 6 |
class MyModelAdmin(models.ModelAdmin): list_display = ('field1', 'field2', 'combined_fields') readonly_fields = ('combined_fields', ) def combined_fields(self, obj): return '{} {}'.format(obj.field1, obj.field2) |