Using django-import-export: How to customise which fields to export Excel files - django

I just starting out using Django. I'm using django-import-export package and I tried to customize my Django admin panel in this page in order to choose which fields to export to excel file.
Here is my admin model
class CompanyAdmin(ImportExportMixin, admin.ModelAdmin):
model = Company
resource_classes = [CompanyResource]
list_display = [
"name",
"uuid",
]
fields = [
"name",
"email",
]
def get_export_resource_kwargs(self, request, *args, **kwargs):
formats = self.get_export_formats()
form = CompanyExportForm(formats, request.POST or None)
form_fields = ("name", "email", )
return {"form_fields": form_fields}
Here is my model resource
class CompanyResource(resources.ModelResource):
class Meta:
model = Company
def __init__(self, form_fields=None):
super().__init__()
self.form_fields = form_fields
def get_export_fields(self):
return [self.fields[f] for f in self.form_fields]
Here is my export form
class CompanyExportForm(ExportForm):
# what should i write here? is this the place that i should write the custom code eg: checkboxes where user have the options to export the 'name` or 'email' fields ??
I try to use the same way as in this post in order to achieve the same result. However, i have been stuck forever.
Update:
For those who ended up here: Please take a look at this blog post : it is a different solution, although there are some improvement that can be done

If you want to define which fields should be exported, refer to this post. This means that only those fields will be exported, and the user cannot choose. This is relatively simple to achieve.
However it seems like you want the user to be able to choose the fields in the UI, then it is more complicated, and will involve more customisation. The answer you link to is the starting point.
There will have to be some UI element in which the user can choose the fields for export (e.g. some multi select widget). Then on POST, you'll have to read the ids of those fields and then feed that into the export() method.
If you are new to Django, it's going to take a bit of work to implement, and will be quite a steep learning curve. If you can find a "clean" way to implement it, such that future users would be able to implement, we would consider a PR.

Related

Displaying fields not intended to be edited in ModelAdmin

I have a custom contact form for which I create a sent_time field using auto_now_add to save the time when the user had sent the message.
I am able to list all the information on the listing view of the admin panel however when I try to enter a specific message I hit the following error:
'sent_time' cannot be specified for GeneralContact model form as it is a non-editable field
My attempt to make the fields readonly in the ModelAdmin results in the same error
class GeneralContactAdmin(ModelAdmin):
"""
Admin model for general correspondence via
the main contact form on the information page
"""
model = GeneralContact
list_display = GeneralContact.__all__
search_fields = GeneralContact.__all__
readonly_fields = GeneralContact.__all__
ordering = ('-sent_time',)
list_filter = ('sent_time', 'has_response')
Surely it is possible to be displayed only, perhaps I've done something incorrectly in my models?
Here is the base model I use for the contact model
class ContactFormBase(models.Model):
__all__ = (
'sent_time', 'sender_name', 'sender_email',
'sender_message', 'has_response', 'responded_on'
)
sent_time = models.DateTimeField(auto_now_add=True)
sender_name = models.CharField()
sender_email = models.EmailField()
sender_message = models.TextField()
has_response = models.BooleanField(
default=False,
help_text='Select whether this message has been replied to by an admin.',
)
responded_on = models.DateTimeField(blank=True, null=True)
panels = [
FieldRowPanel([
FieldPanel('sender_name'),
FieldPanel('sender_email'),
]),
FieldPanel('sent_time'),
FieldPanel('sender_message'),
FieldRowPanel([
FieldPanel('has_response'),
FieldPanel('responded_on'),
])
]
class Meta:
abstract = True
ordering = ['-sent_time',]
The actual class being used is rather plain, perhaps something needs to be done here to allow display of readonly fields?
class GeneralContact(ContactFormBase, models.Model):
panels = ContactFormBase.panels
class Meta:
verbose_name = 'General Contact Entry'
verbose_name_plural = 'General Contact Entries'
In the list view all the information is able to be displayed. In the editing view, ideally there would be all of the information about the message and sender as readonly fields and an option for the admin to change the has_response value based on whether someone has responded or not.
In what way could I achieve this?
update
After seeing this Q&A I have changed the auto_now_add to use django.utils.timezone.now as the default on the sent_time attribute and life seems better, the error from the start of the question is gone and the edit view loads up entirely. However, now all the fields are editable which is not desirable.
Looking into the ModelAdmin class provided by Wagtail it appears that readonly_fields isn't available and perhaps only a feature of the django admin class of the same name. So I'm not sure what to do here. Wagtails HelpPanel type of output is what I'm looking for, and I had an idea to use that to display the data but I'm not sure what that looks like or even how it'd be done as I'm just learning django and wagtail.
update 2
Attempted to use HelpPanel instead of FieldPanel in order to try display the values but seems as if the HelpPanel doesn't retrieve the value of the attributes. Checking through these docs I see the mention of things like djangos readonly_field is not included which confirms why one of my former attempts didn't work but I did find mention of inspect_view_enabled which displays the values in a read only fashion and after trying it out it looks very much how I was trying to get it, alas, nothing there is editable which makes sense but I am getting closer.
I am wondering if a good solution would be to override the view or template used for GeneralContactAdmin but unsure if that's the right way to go about it just to output some text for one class.
A simpler solution is to keep the inspect view and only add the has_response to the edit view, but two views, one of which would only be a checkbox is not a nice for UX.
Surely there is a better way to solve this?

How to design a Django API to handle a "Dynamic" form?

I have built an Angular form that contains a form array. So the user has the control to add fields or delete fields from the form. What I am trying to understand is how I can design a Django API that can handle the post for this kind of dynamic form?
I do not want the user to create an account on the website in order to place his order. Is that even possible?
You should be more concerned about how to model your data, than you can think about your interface. Here a few options for modeling your data:
Option One is to use regular Django ORM, and in this case you may use the JSONField for any dynamic properties.
Option two is to use any schemaless data model, such as document-based databases(e.g MongoDB).
Here a simple example, on how to use Django's JSONField:
your model:
class Order(models.Model):
customer = models.ForeignKey(User, on_delete=models.CASCADE)
#any additional static fields
properties = JSONField()
your view:
def create_order_view(request):
if request.method == "POST":
#do your validation
Order.objects.create(user=request.user, properties=request.POST["properties"])
return Response(status=200)
this example is totally incomplete as you have to add validation error handling, and it is a better idea to use Django rest-framework for constructing your API.
Finally as I said there many option to model your data, in addition to what I mentioned above there are many other. To decide what model to use, you have to know how your data gonna be consumed, so you can optimze for query time, in addition there are many other factors but this is out of scope of this asnwer.
For me, I used Django-RESTframework to build the api.
The way to achieve this is simple, just create the model and iterate through the items which is the dynamic part, and assign the Foreignkey field to obj.id created. First, I created the main model instance, then created the instances of the child instances. I will use Order and Item to demonstrate the idea, The Item instance will have Foreinkey field to Order model.
In the Item model, add "related_name" argument to the Foreinkey field
order = models.ForeignKey(Order, related_name='items',on_delete=models.CASCADE)
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = [
....your fields...
]
class OrderSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True)
class Meta:
model = Order
fields = [
'order', ....
]
def create(self, validated_data):
items_data = validated_data.pop("items")
order = Order.objects.create(**validated_data)
order.total_fees = order.delivery_fees
for item in items_data:
i = Item.objects.create(order=order, **item)
return order

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)

Django admin: don't send all options for a field?

One of my Django admin "edit object" pages started loading very slowly because of a ForeignKey on another object there that has a lot of instances. Is there a way I could tell Django to render the field, but not send any options, because I'm going to pull them via AJAX based on a choice in another SelectBox?
You can set the queryset of that ModelChoiceField to empty in your ModelForm.
class MyAdminForm(forms.ModelForm):
def __init__(self):
self.fields['MY_MODEL_CHOIE_FIELD'].queryset = RelatedModel.objects.empty()
class Meta:
model = MyModel
fields = [...]
I think you can try raw_id_fields
By default, Django’s admin uses a select-box interface () for fields that are ForeignKey. Sometimes you don’t want to incur the overhead of having to select all the related instances to display in the drop-down.
raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField
Or you need to create a custom admin form
MY_CHOICES = (
('', '---------'),
)
class MyAdminForm(forms.ModelForm):
my_field = forms.ChoiceField(choices=MY_CHOICES)
class Meta:
model = MyModel
fields = [...]
class MyAdmin(admin.ModelAdmin):
form = MyAdminForm
Neither of the other answers worked for me, so I read Django's internals and tried on my own:
class EmptySelectWidget(Select):
"""
A class that behaves like Select from django.forms.widgets, but doesn't
display any options other than the empty and selected ones. The remaining
ones can be pulled via AJAX in order to perform chaining and save
bandwidth and time on page generation.
To use it, specify the widget as described here in "Overriding the
default fields":
https://docs.djangoproject.com/en/1.9/topics/forms/modelforms/
This class is related to the following StackOverflow problem:
> One of my Django admin "edit object" pages started loading very slowly
> because of a ForeignKey on another object there that has a lot of
> instances. Is there a way I could tell Django to render the field, but
> not send any options, because I'm going to pull them via AJAX based on
> a choice in another SelectBox?
Source: http://stackoverflow.com/q/37327422/1091116
"""
def render_options(self, *args, **kwargs):
# copy the choices so that we don't risk affecting validation by
# references (I hadn't checked if this works without this trick)
choices_copy = self.choices
self.choices = [('', '---------'), ]
ret = super(EmptySelectWidget, self).render_options(*args, **kwargs)
self.choices = choices_copy
return ret

Can I disable a field in the Rest Framework API browsing view

I am using Django Rest Framework to serialize a model in which I have a foreignkey.
models.py
class Article(models.Model):
author = models.ForeignKey(Author, related_name='articles')
... other fields...
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Article
I want to get rid of the 'HTML form' at the bottom of the browsable API view since I get a list with all my articles and retrieving them from the DB takes ages (I have some 100K articles, and each time the html form is displayed, my server does 100K queries).
I have read the answer from How to disable admin-style browsable interface of django-rest-framework? and I am currently displaying the view in JSON. However, I like the html view and would like to find a way to avoid the html form available at the bottom.
I don't want to properly remove the field from the view (I need to use it), but just remove the database queries used to populate the form.
Any idea ?
Making the field read-only also means you cannot modify it, which is probably not wanted in all cases.
Another solution is to override the BrowsableApiRenderer so it won't display the HTML form (which can be indeed really slow with a lot of data).
This is surprisingly easy, just override get_rendered_html_form:
from rest_framework.renderers import BrowsableAPIRenderer
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_rendered_html_form(self, *args, **kwargs):
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
then adjust your settings to use this renderer:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}
I answer my own question.
I found in the documentation the solution to my problem. I had to use the read_only attribute.
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.RelatedField(read_only=True)
class Meta:
model = Article
fields = ('author', ...other_fields)
#maerteijn answer will disable all forms: POST, PUT, DELETE and OPTIONS.
If you still want to allow the awesome "OPTIONS" button, you can do something like this
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
OPTIONS_METHOD = "OPTIONS"
def get_rendered_html_form(self, data, view, method, request):
if method == self.OPTIONS_METHOD:
return super().get_rendered_html_form(data, view, method, request)
else:
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
And modify settings.py in the same way
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}