Django: using ForeignKeyRawIdWidget outside of admin forms - django

I'm trying to find some documentation of how to use the ForeignKeyRawIdWidget in my own forms. Currently I keep getting the error, "init() takes at least 2 non-keyword arguments (1 given)" which tells me nothing.
Any help would be most appreciated. Googling this turns up little but dev conversations and no examples that I can find of how to implement it.
Update: This is solved; see solution below.

As of the Django 1.5, this works to reuse the ForeignKeyRawIdWidget in non-admin forms.
from django.contrib.admin.sites import site
class InvoiceForm(ModelForm):
class Meta:
model = Invoice
widgets = {
'customer': ForeignKeyRawIdWidget(Invoice._meta.get_field('customer').rel, site),
}
Update
Django 2.0 is deprecating field.rel in favor of field.remote_field. You might want to use this instead (also works on Django 1.11):
...
ForeignKeyRawIdWidget(Invoice._meta.get_field('customer').remote_field, site),
...

This is from the source code (django.contrib.admin.widgets):
class ForeignKeyRawIdWidget(forms.TextInput):
"""
A Widget for displaying ForeignKeys in the "raw_id" interface rather than
in a <select> box.
"""
def __init__(self, rel, attrs=None):
self.rel = rel
super(ForeignKeyRawIdWidget, self).__init__(attrs)
#.....
From the remaining code, I would guess that rel is the foreign key field of your model. At one point, the code checks self.rel.limit_choices_to, and this attribute (limit_choices_to) can only be set on a ForgeinKey field.

Related

How to place captcha on Django CreateView that uses Material Design Layout() feature

I'm working in an existing codebase that uses Django Material. There is a CreateView defined with a Django Material Layout:
class OurModelCreateView(LayoutMixin, CreateView):
model = OurModel
layout = Layout(
Row('field1', 'field2', 'field3'),
Row(...)
)
This view is getting lots of spam signups and so needs to have a captcha. I use Django Recaptcha, and I've set up a number of captchas in the past. However, I've never set one up without using a ModelForm. If I create a Django model form and define the captcha field in the form as I've always done:
from captcha.fields import ReCaptchaField
from captcha.widgets import ReCaptchaV3
class OurModelForm(ModelForm):
captcha = ReCaptchaField(widget=ReCaptchaV3)
class Meta:
model = OurModel
exclude = ()
and then specify form_class = OurModelForm on the CreateView, the following error is raised by ModelFormMixin.get_form_class(): "Specifying both 'fields' and 'form_class' is not permitted". This error is being raised because, though I've not explicitly specified fields, Django Material's LayoutMixin defines fields: https://github.com/viewflow/django-material/blob/294129f7b01a99832a91c48f129cefd02f2fe35f/material/base.py (bottom of the page)
I COULD drop the Material Layout() from the CreateView, but then that would mean having to create an html form to render the Django/Django Material form - less than desirable as there are actually several of these CreateViews that need to have a captcha applied.
So I think that the only way to accomplish what I'm after is to somehow dynamically insert the captcha field into the form.
I've dynamicaly inserted fields into Django forms in the past by placing the field definition in the __init__() of the Django form definition, but I can't figure out what to override in either CreateView (or the various mixins that comprise CreateView) or Django Material's LayoutMixin in order to dynamically insert the captcha field into the form. The following several attempts to override get_form and fields in order to dynamically insert the captcha field do not work:
On the CreateView:
def get_form(self, form_class=None):
form = super(OurModelCreate, self).get_form(form_class)
form.fields['captcha'] = ReCaptchaField(widget=ReCaptchaV3)
return form
def fields(self):
fields = super().fields(*args, **kwargs)
fields['captcha'] = ReCaptchaField(widget=ReCaptchaV3)
return [field.field_name for field in fields
# fields is actually a list, so trying the following too, but it doesn't include the ReCaptchaField(widget=ReCaptchaV3) anywhere at this point
def fields(self):
fields = super().fields(*args, **kwargs)
fields.append('captcha')
return fields
Any help would be greatly appreciated.
Following up on the comment from #Alasdair above which pointed me to the answer, I solved this problem by removing Django Material's LayoutMixin from CreateView, creating a Django form with the captcha field defined, and then adding to CreateView the form_class for the Django form. Also see my last comment above. It was counterintuitive to me until I looked again at the code after #Alasdair's second comment: the use of LayoutMixin on the CreateView isn't necessary for the layout = Layout(...) on the CreateView to work.

Altering the listview of the django admin portal

I've read, extensively, how to change the admin site of Django. I have it mostly figured out -- I think. However there are still a few things that elude me in my understanding. I am using the default registered admin urls; so they are not customized, only what is exposed automatically.
The easiest way to explain this is through imagery...
Here's what I have:
Here's what I want:
I'm fairly certain the changes should be fairly simple. But I don't know exactly which model to alter and template to adjust to get it to look how I want. The [number] -- [name] are fields in my model.
I have extended other pieces of the admin interface to get customized forms for editing particular elements -- by registering my model and customizing the field for it.
#admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
form = CourseAdminForm
fieldsets = (
('Course Info:', {'fields': ('course_number', 'name', 'description', 'units')}),
('Load Info:', {'fields': ('lecture_hours', 'lab_hours', 'discussion_hours', 'work_hours')})
)
in my app/admin.py file.
I'm a bit confused because there technically isn't a model to register here. So I'm not 100% sure how to do this. Do I wrap each one of my modifications inside the CourseAdmin class as different classes/methods with registered URLs or is there some other way I need to be doing this?
You need edit your Course model class:
# models.py
class Course(models.Model):
# fields here
name = ...
# ...
# add a unicode method
# __str__ method if you are using python 3.x
def unicode(self):
return '%s - %s' % (self.pk, self.name)

Extending the Django change list view to include the object title it is related to

I am using the Django comments framework to allow commenting on articles in a blog. I want to display the title of the article that the comment(s) belongs to in the list view of the comments section where the comment name, content type, object id etc is.
How do I do this? I know you can hook up actions into your admin.py list view by writing a model method but in this case I do not have a model for comments since I am using the built in one.
Thanks
Somewhere in your code you can override the Comments ModelAdmin class and extend it to do what you want. This code isn't tested, but it should give you a general enough idea about how to customize the Comment admin:
from django.contrib import admin
from django.contrib.comments.admin import CommentsAdmin
class MyCommentsAdmin(CommentsAdmin):
# The callable that the list_display will call
def show_object_title(self):
return self.content_object.title
list_display = super(MyCommentsAdmin, self).list_display
list_display += ('show_object_title',)
admin.site.unregister(Comment)
admin.site.register(Comment, MyCommentsAdmin)
This doesn't work because the method gets two parameters and you have to use the second to get the title. This works:
def show_object_title(self, obj):
return obj.content_object.title
Also the super()-call doesn't work here. At least not the way described above. It may be easier to just copy and edit the list_display-tuple from the Comments-Admin-Source

Foreign keys in django admin list display

If a django model contains a foreign key field, and if that field is shown in list mode, then it shows up as text, instead of displaying a link to the foreign object.
Is it possible to automatically display all foreign keys as links instead of flat text?
(of course it is possible to do that on a field by field basis, but is there a general method?)
Example:
class Author(models.Model):
...
class Post(models.Model):
author = models.ForeignKey(Author)
Now I choose a ModelAdmin such that the author shows up in list mode:
class PostAdmin(admin.ModelAdmin):
list_display = [..., 'author',...]
Now in list mode, the author field will just use the __unicode__ method of the Author class to display the author. On the top of that I would like a link pointing to the url of the corresponding author in the admin site. Is that possible?
Manual method:
For the sake of completeness, I add the manual method. It would be to add a method author_link in the PostAdmin class:
def author_link(self, item):
return '%s' % (item.id, unicode(item))
author_link.allow_tags = True
That will work for that particular field but that is not what I want. I want a general method to achieve the same effect. (One of the problems is how to figure out automatically the path to an object in the django admin site.)
I was looking for a solution to the same problem and ran across this question... ended up solving it myself. The OP might not be interested anymore but this could still be useful to someone.
from functools import partial
from django.forms import MediaDefiningClass
class ModelAdminWithForeignKeyLinksMetaclass(MediaDefiningClass):
def __getattr__(cls, name):
def foreign_key_link(instance, field):
target = getattr(instance, field)
return u'%s' % (
target._meta.app_label, target._meta.module_name, target.id, unicode(target))
if name[:8] == 'link_to_':
method = partial(foreign_key_link, field=name[8:])
method.__name__ = name[8:]
method.allow_tags = True
setattr(cls, name, method)
return getattr(cls, name)
raise AttributeError
class Book(models.Model):
title = models.CharField()
author = models.ForeignKey(Author)
class BookAdmin(admin.ModelAdmin):
__metaclass__ = ModelAdminWithForeignKeyLinksMetaclass
list_display = ('title', 'link_to_author')
Replace 'partial' with Django's 'curry' if not using python >= 2.5.
I don't think there is a mechanism to do what you want automatically out of the box.
But as far as determining the path to an admin edit page based on the id of an object, all you need are two pieces of information:
a) self.model._meta.app_label
b) self.model._meta.module_name
Then, for instance, to go to the edit page for that model you would do:
'../%s_%s_change/%d' % (self.model._meta.app_label, self.model._meta.module_name, item.id)
Take a look at django.contrib.admin.options.ModelAdmin.get_urls to see how they do it.
I suppose you could have a callable that takes a model name and an id, creates a model of the specified type just to get the label and name (no need to hit the database) and generates the URL a above.
But are you sure you can't get by using inlines? It would make for a better user interface to have all the related components in one page...
Edit:
Inlines (linked to docs) allow an admin interface to display a parent-child relationship in one page instead of breaking it into two.
In the Post/Author example you provided, using inlines would mean that the page for editing Posts would also display an inline form for adding/editing/removing Authors. Much more natural to the end user.
What you can do in your admin list view is create a callable in the Post model that will render a comma separated list of Authors. So you will have your Post list view showing the proper Authors, and you edit the Authors associated to a Post directly in the Post admin interface.
See https://docs.djangoproject.com/en/stable/ref/contrib/admin/#admin-reverse-urls
Example:
from django.utils.html import format_html
def get_admin_change_link(app_label, model_name, obj_id, name):
url = reverse('admin:%s_%s_change' % (app_label, model_name),
args=(obj_id,))
return format_html('%s' % (
url, unicode(name)
))

django pagination - Remembering checked checkboxes across pages

I actually want to accomplish the same thing a user in
Remembering checked checkboxes across pages - what's the best way?
asked. But not for php, I want it for django. Since django is just great :-)
I`m using pagination and want to remember the checked checkbox while the user is navigating over the pages provided by pagination.
Now I'm searching for a best practice how I could accomplish this. I do not really have a good idea how i could accomplish this. My idea would include javascript, but I'm sure there is somewhere out there a solution without js.
I can't think of any way to do this with a paginator. But this is a good spot for a formwizard and dynamic forms. The general idea is to create a dynamic form for each "page" and then a formwizard to hold them all together. This allows for saving the checkboxes across multiple pages and lets you go backwards and forwards easily. The best thing is that it takes virtually no code!
Something like this should be able to deal with everything:
from django.contrib.formtools.wizard import FormWizard
from django import forms
from django.forms.extras.widgets import CheckboxSelectMultiple
from django.core.paginator import Paginator
# In your forms.py --------------
def MPageFormMaker(paginator):
"""
Create a "paginated" group of forms based on a queryset and num-per-page item.
"""
def Sform(this_q):
"""
Create this page's items
"""
class _PageForm(forms.Form):
items = forms.ModelMultipleChoiceField(queryset = this_q,
widget = forms.CheckboxSelectMultiple)
return _PageForm
for i in range(paginator.num_pages):
yield Sform(paginator.page(i).object_list)
class MpageForm(FormWizard):
def done(self, request, formlist):
#do something with the list of forms
#----- In your views.py
def MpageChecker(request):
qset = Item.objects.all()
paginator = Paginator(qset, 30)
formwizard = MPageForm(list(MPageFormMaker(paginator)))
#then deal with it like a normal formwizard
Essentially just instantiate the formwizard class and then let it take care of everything. Since it uses a paginator class to make the forms you can use any sort of personalization you'd like.
BTW: I haven't tested this code so it may have a few typos but it should be enough to get you on your way.
EDIT ... fix the ordering problem, now all ordering should be preserved correctly across pages!