Additional fields in Django admin interface - python

Suppose I have some persistent model property that's not backed up by a model field, how do I allow editing this field in the admin interface?
Example setup:
# models.py
# appropriate imports...
class MyModel(Model):
# regular fields.
foo = CharField(max_length=50)
bar = BooleanField()
# ...
# custom property, backed by a file.
def _get_meh ( self ):
return ... # read from file
def _set_meh ( self, value ):
... # write to file.
meh = property(_get_meh, _set_meh)
meh's value is actually stored in a file on disk who's path depends on the value in foo. I'd like to be able to edit meh's value from the admin interface.
# admin.py
# appropriate imports...
class MyModelAdmin(ModelAdmin):
# what do I put here?
Note: in case someone needs to ask, I'm using Django version 1.2.1, but upgrading is possible if that is required by your solution. Anything that runs on Python 2.5 will do, this I can't really upgrade for the moment.

Take a look at this:
http://www.scribd.com/doc/68396387/Adding-extra-fields-to-a-model-form-in-Django%E2%80%99s-admin-%E2%80%93-Hindsight-Labs
(this one went offline, http://www.hindsightlabs.com/blog/2010/02/11/adding-extra-fields-to-a-model-form-in-djangos-admin/)
Basically, you'll create a MyModelFrom subclassed from forms.ModelForm and:
Add the extra meh fields to the MyModelFrom definition.
Override the constructor of the MyModelFrom, set the self.initial meh property if a model instance was passed to the form.
Override the save method of the MyModelFrom, set the meh property on the model instance based on the form fields.
This way, the meh property would be correctly filled at your Model, but you'll also need to override MyModel's save() method to actually persist this value to your disk file:
(Google for "django model override save", sry, it seems I can't post more than a link per answer till I get more than 10 rep...)
Cheers,

Ny Django knowledge isn't that great, but depending on how complicated you want it to be, I'm not sure something like this can be easily done without much hackery.
Anyhow, if you want to add it to Add MyModel page, you can edit the appropriate admin template.
So in your template directory, create something like:
admin/app_label/MyModel/change_form.html
Then extend the base template, and add your own content:
{% extends "admin/change_form.html" %}
{% block something_you_want_to_override %}
<p>Your HTML goes here</p>
{% endblock %}
Is there really no way you can get this custom field into an actual Django field though? Surely you can override the save() method on the model and do it that way? Or use a pre_save signal?
Cheers,
Victor

Related

How to modify Page model view in wagtail admin?

Background: I would like to enhance a page instance during an admin page view with some admin request related information (some pre-population in general). Basically I would need some function like "get_queryset", but not for list view, just for edit view.
In my older question related to a similar problem: Wagtail - how to preopulate fields in admin form? I was provided with instructions to use something called
CreatePageView
However, I cannot import it. Furthermore, I cannot even found any mention about that in google if I search:
Wagtail +CreatePageView
The closest thing I found is https://docs.wagtail.io/en/v2.1.1/reference/contrib/modeladmin/create_edit_delete_views.html but the page also states:
NOTE: modeladmin only provides ‘create’, ‘edit’ and ‘delete’
functionality for non page type models (i.e. models that do not extend
wagtailcore.models.Page). If your model is a ‘page type’ model,
customising any of the following will not have any effect
I am quite confused. What should I do if I need to customize the admin view for Page model extension?
I studied the wagtail source codes for Model.admin and Page and I have not found any way. Any ideas?
The related code simplified:
wagtail hooks:
class ItemAdmin(ModelAdmin):
pass
# some function override here maybe?
models:
class ItemPage(Page):
pass
# override for a function that gives data to the admin view maybe here?
Edit
As suggested in comments, it is possible to modify the admin page form during creation:
from wagtail.admin.forms import WagtailAdminPageForm
class ItemPageForm(WagtailAdminPageForm):
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super().__init__(data, files, *args, **kwargs)
class ItemPage(Page):
base_form_class = ItemPageForm
however, acquiring the "request" in the WagtailAdminPageForm constructor does not seem possible.
This question is a bit ambiguous, so it is not super clear exactly what you need.
Interpreted question: When crediting (or editing) a page, I need access to the request to modify the initial values of some fields in the page form.
Potential Approach
Note: This may not be best practice and could be fragile depending on future changes to Wagtail.
First, we need a custom EditHandler, these are the way Wagtail builds up forms and even Panels within the editing interface. An EditHandler's job is to manage the form to return based on the model and even the current request.
As a first step, it would be good to get your page create form showing correctly by following the instructions on using a custom tabbed interface. From here, you can replace the TabbedInterface with your custom class (e.g. CustomTabbedInterface) and add some functionality to this which will allow for a dynamic form_class to be returned.
get_form_class should return the form_class, however, we can modify it to return a function that, when called, will instantiate the class with custom information based on the request.
There may be some issues with this approach below in edit views or scenarios not considered by this example so validate this fully before using.
Example Code
from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
from wagtail.core.models import Page
class CustomTabbedInterface(TabbedInterface):
def get_form_class(self):
form_class = super().get_form_class()
request = self.request
if request and request.method != 'POST':
# check request is available to ensure this instance has been bound to it
user = self.request.user
def initiate_class(**kwargs):
# instead of returning the class, return a function that returns the instantiated class
# here we can inject a kwarg `initial` into the generated form
# important: this gets called for edit view also and initial will override the instance data
# kwarg['instance'] will be the `Page` instance and can be inspected as needed
kwargs['initial'] = {'introduction': user.first_name}
return form_class(**kwargs)
return initiate_class
return form_class
class StandardPage(Page):
# ... field etc
edit_handler = CustomTabbedInterface([
ObjectList(content_panels, heading='Content'),
ObjectList(Page.promote_panels, heading='Promote'),
ObjectList(Page.settings_panels, heading='Settings', classname="settings"),
])
Explanation
wagtail/admin/views/pages.py contains the create view, which will use the edit_handler, bind it to the model and the request and then call its get_form_class.
The form_class is used for the response here form = form_class(instance=page, parent_page=parent_page)
It gets called with the instance and the parent_page kwargs
Our custom get_form_class response takes those kwargs and injects an additional initial kwarg.
initial is used by Django forms to add any initial data - https://docs.djangoproject.com/en/3.0/ref/forms/api/#dynamic-initial-values
Finally, the Django form will merge the instance field values with the intial kwarg to generate the final pre-filled data for the form. You can see how this works in Django's BaseModelForm.
Be careful to consider what will happen on an update view, you likely do not want to override existing values with your initial values when a user has already entered something in the field.

Display icons in Django admin for each item

I want to add a specified icon for each model on admin index page. I added an attribute named "picture" on each model then I modified /contrib/admin/sites.py to pass that picture name to template and checked and use it on index.html template of admin to get the result.
I wonder to know if there is a better way
class Product(models.Model):
abbr = models.CharField(max_length=20,unique=True)
title = models.CharField(max_length=200,unique=True)
owner = models.ForeignKey(UserProxy)
des = models.TextField(blank=True,null=True)
picture = 'product.png'
def __unicode__(self):
return self.abbr
class Meta:
none
What You did seems OK, only small tips that can make Your code a little better:
Instead of modifying django/contrib/admin/sites.py You can subclass the AdminSite class (if You didn't do that already).
Modify the AdminSite.index() method to pass not picture, but whole admin class (there is a model_admin variable available in the index() method).
Assign picture in the ModelAdmin classes, not models, to separate admin stuff from models.
The answer from 'Python Fanboy' was so brief but useful to me, I could avoid modifying django base classes
picture field were moved to admin class
I subclass AdminSite as CustomAdminSite and copied index and app_index and made modification
(I don't know if there is a better way than copying whole of index and app_index like overriding)
In urls.py, 'admin.sites.url' replaced by 'custom_site.url'
(custom_site is instance of CustomAdminSite)
I did not want another url instead of '/admin' like '/my_admin' so I have to use instance of CustomAdminSite for all my models even User, Group & Site
I'm using admin_tools, now I've lose my application menu
Any better Idea or solution for my new encountered problems?

Django Admin: when displaying an object, display a URL that contains one of the fields

Here is an abstract base class for many of my "Treatment" models (TreatmentA, TreatmentB, etc):
class TreatmentBase(models.Model):
URL_PREFIX = '' # child classes define this string
code = shared.models.common.RandomCharField(length=6)
class Meta:
abstract = True
Each Treatment instance has a URL, that when visited by a user, takes them to a page specific to that treatment. I want to be able to create a Treatment in Django Admin, and immediately get this URL so I can send it to users. This URL can be created with the following method on TreatmentBase:
def get_url(self):
return '{}/{}/'.format(self.URL_PREFIX, self.code)
However, I am stuck with how to get this URL to display in Django Admin. I can think of the following solutions:
(1) Customize the display of the code field so that it becomes a clickable URL. Problem: I don't know how to do this.
(2) Add the get_url method to ModelAdmin.list_display. Problem: This means I would have to define a separate list_display for each of the child models of BaseTreatment, and I would have to explicitly list all the fields of the model, meaning I have to update it every time I modify a model, violating DRY.
(3) Add an extra field like this:
url = models.URLField(default = get_url)
Problem: get_url is an instance method (since it needs to refer to the self.code field), and from my reading of the docs about the default argument, it just has to be a simple callable without arguments.
Any way to do this seemingly simple task?
You could go with option 2 (adding to the admin display) but add it to the
readonly_fields which may alleviate your DRY concerns when models changes.
Option 3 (the extra field) could also work if you override the save method setting the URL property. You'd either want to set the field as readonly in the admin or only set the value in the save method if it's currently None.

Override a Django form field from a different app

Django Guardian has two forms defined in admin.py, GroupManage and UserManage: https://github.com/lukaszb/django-guardian/blob/master/guardian/admin.py#L368
I would like to add auto-completion to these two forms, and the best way I assume to make that happen is to overwrite the group and user's field widgets (my first attempt uses django autocomplete_light.) The goal is to not need to fork django guardian.
So in my app's models.py, I added the following code
GroupManage.__class__.group = forms.CharField(max_length=81,
error_messages={'does_not_exist':
"This group does not exist!"}, widget=ChoiceWidget(True))
I also tried using setattr to no avail. In the django shell it acts like this should be working, but when the admin page gets loaded the old group variable is restored, with the default CharField widget.
The fields defined for the class are stored in the dictionary base_fields.
GroupManage.base_fields['group'] = forms.CharField(max_length=81,
error_messages={'does_not_exist':
"This group does not exist!"}, widget=ChoiceWidget(True))
Sometimes, it might be easier to alter a field attribute instead of replacing the entire field:
GroupManage.base_fields['group'].help_text = "New help text"

Django Form with extra information

I'm have a model that has some fields that should never be edited, such as "date created." I want to display a form that has some text fields with the extra information, probably just the data itself in a <span>. A person can edit a few fields and at the same time they can see the extra information about the instance of the model, such as created, last modified, or even related objects in the database for this object. Is there a way to do this using Django's built in framework, or will I have to create my own custom form by passing the whole object to a template?
If this needs more clarification, feel free to ask.
The best way I know to do this is to initialize the fields before you pass the form to the template by passing an initial dictionary to the form or by passing a instance object to the form.
You should then make sure that the fields are disabled, or you should make them hidden fields and then display the fields as regular text.
Most importantly, if you're passing data to the client that will then be sent back in a form, you should make sure that the data coming in is the same as the data that went out (for security's sake). Do this with at clean_[field] function on the Form. It should look like the following.
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def clean_date_created(self):
if self.cleaned_fields['date_created'] != self.instance.date_created:
raise ValidationError, 'date_created has been tampered'
self.cleaned_fields['date_created']
[Edit/Addendum] Alternatively, you can pass the data directly to your template to render separately, and then tack on the data to your form after you get it back into your view. It should go something like this:
def recieve_form(request, ...):
...
f = MyForm(request.POST, instance=a)
new_model_instance = f.save(commit=False)
new_model_instance.date_created = <whatever>
new_model_instance.save()
To do that I usually customize the form in order for the widget to be read only, like the following:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
widgets = {
'my_field': forms.TextInput(attrs={'disabled':'disabled'}),
}
This will output a read only text field but I guess you can also create a Widget that will just output a <div> or a <p> (or whatever you need)

Categories

Resources