I have an extended admin model that creates action buttons. I have created a view to do pretty much the same thing. I have used tables2 and everything is just fine except for the actions column. I cannot find a way to generate the same button in the table. Is there a way to do this at all?
tables.py
from .models import Ticket
import django_tables2 as tables
'''from .admin import river_actions, create_river_button'''
class TicketTable(tables.Table):
class Meta:
model=Ticket
template_name='django_tables2/table.html'
fields = ('id','subject','request_type','material_type','productline','business','measurement_system',
'created_at','updated_at','status','river_action','project') # fields to display
attrs = {'class': 'mytable'}
'''attrs = {"class": "table-striped table-bordered"}'''
empty_text = "There are no tickets matching the search criteria..."
admin.py (the part that includes the model etc)
# Define a new User admin to get client info too while defining users
class UserAdmin(BaseUserAdmin):
inlines = (UserExtendInline, )
def create_river_button(obj,proceeding):
return '''
<input
type="button"
style=margin:2px;2px;2px;2px;"
value="%s"
onclick="location.href=\'%s\'"
/>
'''%(proceeding.meta.transition,
reverse('proceed_ticket',kwargs={'ticket_id':obj.pk, 'next_state_id':proceeding.meta.transition.destination_state.pk})
)
class TicketAdmin(admin.ModelAdmin):
list_display=('id','client','subject','request_type','material_type','productline','business','measurement_system', \
'created_at','updated_at','created_by','status','river_actions')
#list_display_links=None if has_model_permissions(request.user,Ticket,['view_ticket'],'mmrapp')==True else list_display
#list_display_links=list_display #use None to remove all links, or use a list to make some fields clickable
#search_fields = ('subject','material_type')
list_filter=[item for item in list_display if item!='river_actions'] #exclude river_actions since it is not related to a field and cannot be filtered
#Using fieldset, we can control which fields should be filled by the user in the ADD method. This way, created_by will be the
#logged in user and not a drop down choice on the admin site
fieldsets = [
(None, {
'fields': ('client','subject','description','request_type','material_type', \
'productline','business','measurement_system', 'project')
} ), #to make some field appear horizontal, put them into a []
]
formfield_overrides = {
models.CharField: {'widget': TextInput (attrs={'size':'40'})},
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':80})},}
def get_list_display(self, request):
self.user=request.user
return super(TicketAdmin,self).get_list_display(request)
def river_actions(self,obj):
content=""
for proceeding in obj.get_available_proceedings(self.user):
content+=create_river_button(obj, proceeding)
return content
river_actions.allow_tags=True
#override save_model method to save current user since it is not on the admin page form anymore
def save_model(self, request, obj, form, change):
if not change:
# the object is being created and not changed, so set the user
obj.created_by = request.user
obj.save()
views.py
def display_tickets(request):
table = TicketTable(Ticket.objects.all())
RequestConfig(request).configure(table)
'''table = CustomerTable(Customer.objects.filter(self.kwargs['company']).order_by('-pk'))'''
return render(request,'mmrapp/ticket_display.html',{'table':table})
buttons created in admin page:
table created using tables2 in views missing buttons:
You must pass empty_values=() to the column, because by default, django-tables2 only renders the column if the value is not contained in empty_values for that column.
import django_tables2 as tables
from .admin import river_actions, create_river_button
from .models import Ticket
class TicketTable(tables.Table):
river_action = tables.Column(empty_values=())
class Meta:
model=Ticket
template_name='django_tables2/table.html'
fields = (
'id', 'subject', 'request_type', 'material_type', 'productline', 'business', 'measurement_system',
'created_at', 'updated_at', 'status', 'river_action', 'project'
) # fields to display
attrs = {'class': 'mytable'}
empty_text = "There are no tickets matching the search criteria..."
def render_river_action(self, record):
return create_river_button(record, ...)
This is also documented as Table.render_foo methods
Related
I have a Django input slider defined as follows:
#widgets.py
from django.forms.widgets import NumberInput
class RangeInput(NumberInput):
input_type = 'range'
#forms.py
from polls.widgets import RangeInput
class VoteForm(forms.ModelForm):
class Meta:
#model = CC_Responses
model = CC_Resp_NoFK
fields = ['Person_ID', 'Test_date', 'Response_value']
# following added from SliderModelForm
widgets ={
'Response_value':RangeInput
}
which is “processed” by the view
def p2vote(request,q_id):
CC_question = get_object_or_404(CC_Questions, pk=q_id)
#
if request.method == 'POST':
form = VoteForm(request.POST)
if form.is_valid():
item = form.save(commit=False)
item.Q_ID = q_id
item.save()
return redirect('/polls/p2')
else:
formV = VoteForm()
return render(request, 'pollapp2/vote.html', {'var_name':CC_question,'form' : VoteForm()})
and in the template I have the inline CSS
<style>
/* Following CSS is used by the input slider (range) since Django assigns id value of id_Response_value */
/*See: https://stackoverflow.com/questions/110378/change-the-width-of-form-elements-created-with-modelform-in-django */
#id_Response_value{width:300px;}
</style>
which is associated with the slider/range
<label for="Response_value">Response value</label>
{% render_field form.Response_value rows="1" class="form-control" %}
I obtained id_Response_value by looking at the source of the rendered HTML in the browser – I guess I could explicitly set an ID. All the above works exactly as I want. I can control the width of the range slider using CSS.
Now, I believe, inline CSS is a “bad thing”, so as a step to improve my code I’m trying to associate the attribute “more directly” with the slider.
In Django documentation in the section: https://docs.djangoproject.com/en/4.0/topics/forms/modelforms/#overriding-the-default-fields
there is the following example:
from django.forms import ModelForm, Textarea
from myapp.models import Author
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}),
}
The dictionary attrs created looks like it has CSS “stuff” in it, so I coded in widgets in the model form in forms.py
from polls.widgets import RangeInput
class VoteForm(forms.ModelForm):
class Meta:
model = CC_Resp_NoFK
fields = ['Person_ID', 'Test_date', 'Response_value']
widgets ={
'Response_value':RangeInput(attrs={'width': '1000px'}),
}
I tried both:
attrs={'width': '1000px'}
and
attrs={'width': 1000}
Neither changed the width of the range slider. Is what I am doing possible in the (model)form? Have I just got a problem with what I am coding in attrs?
I may have misunderstood but I see something like the following promoted as what needs to go in the ModelForm
def __init__(self, *args, **kwargs):
super(ProductForm, self).__init__(*args, **kwargs) # Call to ModelForm constructor
self.fields['long_desc'].widget.attrs['cols'] = 10
self.fields['long_desc'].widget.attrs['rows'] = 20
No. Really? To change the attributes?
Is there any chance to add link to change page of a Foreign key object next to 'raw_id_fields' in django admin?
I use raw_id_fields to reduce number of queries to mysql.
I could edit or add a foreign key object when I didn't use raw_id_fields.
How to do it with raw_id_fields?
#AntoinePinsard, I've tried the second solution. The field 'Edit My FK' was created but there is nothing except for a hyphen.
That's my modeladmin.
class FlatAdmin(admin.ModelAdmin):
inlines = [NeedInline]
readonly_fields = ('flat_house_edit_link',)
raw_id_fields = ('flat_house',)
list_display = ('show_date','show_block','show_house','show_rooms','show_price','show_stage')
list_filter = ('flat_house__house_block','flat_rooms')
search_fields = ('flat_house__id',)
field = ['flat_house','flat_house_edit_link','flat_owner','flat_price','flat_rooms','flat_total_sq','flat_life_sq','flat_kitchen_sq','flat_floors']
def flat_house_edit_link(self, instance):
if instance:
fk_id = instance.user_id
else:
fk_id = None
if fk_id:
opts = instance._meta.get_field('flat_house').rel.model._meta
related_url = reverse(
'admin:{}_{}_change/?_to_field=id&_popup=1'.format(
opts.ha,
opts.house,
),
args=[fk_id],
)
return format_html(
'<a target=_blank href="{}">Go!</a>', related_url)
else:
return "No related object"
flat_house_edit_link.short_description = "Change house"
admin.site.register(Flat,FlatAdmin)
Note This behavior is now built-in since Django 1.10.
You can create a custom widget to render this field as you would like to.
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.html import format_html
class ForeignKeyLinkedRawIdWidget(ForeignKeyRawIdWidget):
def render(self, name, value, attrs=None):
output = super().render(name, value, attrs)
try:
related_url = reverse(
'admin:{}_{}_change'.format(
self.rel.model._meta.app_label,
self.rel.model._meta.model_name,
),
args=[value],
)
except NoReverseMatch:
return output
return format_html('{output} edit',
output=output, url=related_url)
And use this widget in your form, rather than using raw_id_fields:
from myapp.forms.widgets import ForeignKeyLinkedRawIdWidget
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = '__all__'
widgets = {
'my_foreignkey': ForeignKeyLinkedRawIdWidget(),
}
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
However, it would be much simpler to add it as a "fake" readonly_field below the actual field:
from django.core.urlresolvers import reverse
from django.utils.html import format_html
class MyModelAdmin(admin.ModelAdmin):
readonly_fields = ['my_foreignkey_link']
raw_id_fields = ['my_foreignkey']
fields = [..., 'my_foreignkey', 'my_foreignkey_link', ...]
def my_foreignkey_link(self, instance):
if instance:
fk_id = instance.my_foreignkey_id
else:
fk_id = None
if fk_id:
opts = instance._meta.get_field('my_foreignkey').rel.model._meta
related_url = reverse(
'admin:{}_{}_change'.format(
opts.app_label,
opts.model_name,
),
args=[fk_id],
)
return format_html(
'<a target=_blank href="{}">Go!</a>', related_url)
else:
return "No related object"
my_foreignkey_link.short_description = "Edit My FK"
Further reading: https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields
Here's a snippet page (updated for 6 years) that does it for me:
https://djangosnippets.org/snippets/2217/
Just use ImproveRawIdFieldsForm in place of ModelAdmin and all raw_id fields automatically links the object name displayed next to the id input.
I'm using django-autocomplete-light in a django admin application but i cant get choiches correctly filtered for a fk field with limit_choiches_to argument: I still get the entire queryset. here's the code:
# autocomplete_light.py
from django.db.models import Q
import autocomplete_light
from myapp.models import MyClass
from otherapp.models import Deps
class MyClassAutocomplete(autocomplete_light.AutocompleteModelBase):
""" MyClass autocomplete widget class """
choiches = MyModels.objects.filter(
Q(dpt__in=Deps.MAIN_DEPARTMENTS),
Q(user__is_active=True)
)
search_fields = ['^full_name', 'initials']
attrs = {'placeholder': 'Type a name'}
autocomplete_light.register(MyClass, MyClassAutocomplete)
# admin.py
class SampleModelAdminForm(forms.ModelForm):
class Meta:
link_attrs = {'cols': 105, 'rows': 3}
model = SampleModel
def __init__(self, *args, **kwargs):
super(SampleModelAdminForm, self).__init__(
*args, **kwargs
)
self.fields['my_fk'].widget = autocomplete_light.ChoiceWidget(
'MyClassAutocomplete'
)
I also tried to override choices_for_request method in AutocompleteModelBase subclass:
def choices_for_request(self):
return MyModels.objects.filter(
Q(dpt__in=Deps.MAIN_DEPARTMENTS),
Q(user__is_active=True)
)
By this way I have the filtered queryset, but I loose the autocomplete feature (for every word that I type, e.g. 'Es', it starts to show me the choiches from the A letter)
Anybody can help me with that?
thanks
Typo: choiches in
choiches = MyModels.objects.filter(
So I'm having a bit of trouble with trying to create a model that will define dynamic proxy models that manage a related model in the admin site. I know that sentence was confusing, so I'll just share my code instead.
models.py
class Cateogry(models.Model):
name = models.CharField(...)
class Tag(models.Model):
name = models.CharField(...)
category = models.ForeignKey(Cateogry)
What I want to achieve is that in the admin site, instead of having one ModelAdmin for the Tag model, for each category I will have a modeladmin for all related tags. I have achieved this using this answer. Say I have a category named A:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class CatA(TagAdmin):
def queryset(self, request):
qs = super(CatA, self).queryset(request)
return qs.filter(cateogry = Cateogry.objects.filter(name='A'))
create_modeladmin(CatA, name='CategoryAtags', model=Tag)
But this is not good enough, because obviously I still need to manually subclass the TagAdmin model and then run create_modeladmin. What I need to do, is loop over all Category objects, for each one create a dynamic subclass for Tagadmin (named after the category), then create a dynamic proxy model from that, and this is where my head starts spinning.
for cat in Category.objects.all():
NewSubClass = #somehow create subclass of TagAdmin, the name should be '<cat.name>Admin' instead of NewSubClass
create_modeladmin(NewSubClass, name=cat.name, model=Tag)
Any guidance or help would be much appreciated
Dynamic ModelAdmins don't work well together with the way admin registeres models.
I suggest to create subviews in the CategoryAdmin.
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin.options import csrf_protect_m
from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse
from demo_project.demo.models import Category, Tag
class TagAdmin(admin.ModelAdmin):
# as long as the CategoryTagAdmin class has no custom change_list template
# there needs to be a default admin for Tags
pass
admin.site.register(Tag, TagAdmin)
class CategoryTagAdmin(admin.ModelAdmin):
""" A ModelAdmin invoked by a CategoryAdmin"""
read_only_fields = ('category',)
def __init__(self, model, admin_site, category_admin, category_id):
self.model = model
self.admin_site = admin_site
self.category_admin = category_admin
self.category_id = category_id
super(CategoryTagAdmin, self).__init__(model, admin_site)
def queryset(self, request):
return super(CategoryTagAdmin, self).queryset(request).filter(category=self.category_id)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'tag_changelist_link')
def tag_changelist_link(self, obj):
info = self.model._meta.app_label, self.model._meta.module_name
return '<a href="%s" >Tags</a>' % reverse('admin:%s_%s_taglist' % info, args=(obj.id,))
tag_changelist_link.allow_tags = True
tag_changelist_link.short_description = 'Tags'
#csrf_protect_m
def tag_changelist(self, request, *args, **kwargs):
obj_id = unquote(args[0])
info = self.model._meta.app_label, self.model._meta.module_name
category = self.get_object(request, obj_id)
tag_admin = CategoryTagAdmin(Tag, self.admin_site, self, category_id=obj_id )
extra_context = {
'parent': {
'has_change_permission': self.has_change_permission(request, obj_id),
'opts': self.model._meta,
'object': category,
},
}
return tag_admin.changelist_view(request, extra_context)
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.module_name
urls= patterns('',
url(r'^(.+)/tags/$', self.admin_site.admin_view(self.tag_changelist), name='%s_%s_taglist' % info )
)
return urls + super(CategoryAdmin, self).get_urls()
admin.site.register(Category, CategoryAdmin)
The items in the categories changelist have an extra column with a link made by the tag_changelist_link pointing to the CategoryAdmin.tag_changelist. This method creates a CategoryTagAdmin instance with some extras and returns its changelist_view.
This way you have a filtered tag changelist on every category. To fix the breadcrumbs of the tag_changelist view you need to set the CategoryTagAdmin.change_list_template to a own template that {% extends 'admin/change_list.html' %} and overwrites the {% block breadcrumbs %}. That is where you will need the parent variable from the extra_context to create the correct urls.
If you plan to implement a tag_changeview and tag_addview method you need to make sure that the links rendered in variouse admin templates point to the right url (e.g. calling the change_view with a form_url as paramter).
A save_model method on the CategoryTagAdmin can set the default category when adding new tags.
def save_model(self, request, obj, form, change):
obj.category_id = self.category_id
super(CategoryTagAdmin, self).__init__(request, obj, form, change)
If you still want to stick to the apache restart aproach ... Yes you can restart Django. It depends on how you are deploying the instance.
On an apache you can touch the wsgi file that will reload the instance os.utime(path/to/wsgi.py.
When using uwsgi you can use uwsgi.reload().
You can check the source code of Rosetta how they are restarting the instance after the save translations (views.py).
So I found a half-solution.
def create_subclass(baseclass, name):
class Meta:
app_label = 'fun'
attrs = {'__module__': '', 'Meta': Meta, 'cat': name }
newsub = type(name, (baseclass,), attrs)
return newsub
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'category')
def get_queryset(self, request):
return Tag.objects.filter(category = Category.objects.filter(name=self.cat))
for cat in Category.objects.all():
newsub = create_subclass(TagAdmin, str(cat.name))
create_modeladmin(newsub, model=Tag, name=str(cat.name))
It's working. But every time you add a new category, you need to refresh the server before it shows up (because admin.py is evaluated at runtime). Does anyone know a decent solution to this?
the situation
In my example I want to create a Page model with a many to many relationship with a content-blocks model.
A page has a title, slug, and main content block.
content blocks have a title and a content block.
What I can get:
Showing page.blocks in the admin form displays a multi select of content blocks
Creating an inline form for the content blocks on the page admin shows several selects with a + sign to add more
What I am trying to accomplish:
Full CRUD on content block on the page admin
Note: Due to the difficulty of my request, I'm beginning to believe the UX pattern im trying to accomplish is wrong. If I want a content creator to come in and create a page, pick some existing content blocks (ex: an existing sidebar content block), and then create a new custom block. I don't think i want him to have to jump all over the place to do this...
Related Question without solutions:
How do I use a TabularInline with editable fields on a ManyToMany relationship?
EDIT
my admin.py
from django.contrib import admin
from django.contrib.flatpages.admin import FlatpageForm, FlatPageAdmin
from django.contrib.flatpages.models import FlatPage
from my_flatpages.models import ExtendedFlatPage, ContentBlock
from mptt.admin import MPTTModelAdmin
from django import forms
import settings
"""
Extended Flatpage Form
"""
class ExtendedFlatPageForm(FlatpageForm):
class Meta:
model = ExtendedFlatPage
"""
Page Content Block inline form
"""
class ContentBlockInlineAdminForm(forms.ModelForm):
# Add form field for selecting an existing content block
content_block_choices = [('', 'New...')]
content_block_choices.extend([(c.id, c) for c in ContentBlock.objects.all()])
content_blocks = forms.ChoiceField(choices=content_block_choices, label='Content Block')
def __init(self, *args, **kwargs):
super(ContentBlockInlineAdminForm, self).__init__(*args, **kwargs)
# Show as existing content block if it already exists
if self.instance.pk:
self.fields['content_block'].initial = self.instance.pk
self.fields['title'].initial = ''
self.fields['content'].initial = ''
# Make title and content not required so user can opt to select existing content block
self.fields['title'].required = False
self.fields['content'].required = False
def clean(self):
content_block = self.cleaned_data.get('content_block')
title = self.cleaned_data.get('title')
content = self.cleaned_data.get('content')
# Validate that either user has selected existing content block or entered info for new content block
if not content_block and not title and not content:
raise forms.ValidationError('You must either select an existing content block or enter the title and content for a new content block')
"""
Content Block Inline Admin
"""
class ContentBlockInlineAdmin(admin.TabularInline):
form = ContentBlockInlineAdminForm
class Meta:
model = ContentBlock
extra = 1
"""
Extended Flatpage Admin
"""
class ExtendedFlatPageAdmin(FlatPageAdmin, MPTTModelAdmin):
form = ExtendedFlatPageForm
fieldsets = (
(
None,
{
'fields': ('url', 'title', 'content', ('parent', 'sites'))
}
),
(
'SEO Fields',
{
'fields': ('seo_title', 'seo_keywords', 'seo_description'),
'classes': ('collapse', )
}
),
(
'Advanced options',
{
'fields': ('enable_comments', 'registration_required', 'template_name'),
'classes': ('collapse', )
}
),
)
inlines = (ContentBlockInlineAdmin,)
class Media:
js = (
'https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js',
settings.MEDIA_URL + 'js/tinymce/jquery.tinymce.js',
settings.MEDIA_URL + 'js/init_tinymce.js'
)
admin.site.unregister(FlatPage)
admin.site.register(ExtendedFlatPage, ExtendedFlatPageAdmin)
Haven't had the opportunity to test this, but it should work:
class ContentBlockInlineAdminForm(forms.ModelForm):
# Add form field for selecting an existing content block
content_block_choices = [('', 'New...')]
content_block_choices.extend([(c.id, c) for c in ContentBlock.objects.all()])
content_blocks = forms.ChoiceField(choices=content_block_choices, label='Content Block')
def __init(self, *args, **kwargs):
super(ContentBlockInlineAdminForm, self).__init__(*args, **kwargs)
# Show as existing content block if it already exists
if self.instance.pk:
self.fields['content_block'].initial = self.instance.pk
self.fields['title'].initial = ''
self.fields['content'].initial = ''
# Make title and content not required so user can opt to select existing content block
self.fields['title'].required = False
self.fields['content'].required = False
def clean(self):
content_block = self.cleaned_data.get('content_block')
title = self.cleaned_data.get('title')
content = self.cleaned_data.get('content')
# Validate that either user has selected existing content block or entered info for new content block
if not content_block and not title and not content:
raise forms.ValidationError('You must either select an existing content block or enter the title and content for a new content block')
class ContentBlockInlineAdmin(admin.TabularInline):
form = ContentBlockInlineAdminForm
class Meta:
model = ContentBlock
extra = 1
class PageAdmin(admin.ModelAdmin):
inlines = [
ContentBlockInlineAdmin,
]
"""
Override saving of formset so that if a form has an existing content block selected, it
sets the form instance to have the pk of that existing object (resulting in update rather
than create). Also need to set all the fields on ContentType so the update doesn't change
the existing obj.
"""
def save_formset(self, request, form, formset, change):
for form in formset:
if form.cleaned_data.get('content_block'):
content_block = ContentBlock.objects.get(pk=form.cleaned_data.get('content_block'))
instance = form.save(commit=False)
instance.pk = content_block.pk
instance.title = content_block.title
instance.content = content_block.content
instance.save()
else:
form.save()
You could then actually add some javascript to show/hide the ContentBlock fields depending on whether the content_block field is set to 'New..' or an existing one.
This isn't the answer I was looking for, BUT, What I ended up going with is
class Page(models.Model):
....
class ContentBlock(models.Model):
page = models.ForeignKey(
Page,
blank = True,
null = True,
)
....
and then having a regular tabular inline for ContentBlock on the page admin form.
So that way I can have page specific content blocks related to a page, AND be able to have generic content blocks able to be used wherever.
Then, I created an inclusion tag to render a content block by name that I use in my templates.
The project https://github.com/caktus/django-pagelets sounds like exactly what you are looking for. A page can have 'pagelets' and 'shared pagelets' with a nice admin for the two (pagelets are simply content blocks).
The non-shared pagelets are shown as inlines with the ability to add extra blocks directly on the page admin screen. For shared pagelets you get the drop-down with a plus-sign.