I try to render a django-simple-history queryset in a django-tables2 table. At the moment I pass the raw queryset to the template inside the context. Further I want to pass the Queryset to a Table object to use the features of the table like linkyfy columns or exclude columns. For this I have to specify a model inside the tables meta. The problem here is, that the model for the history is generated automatically.
Actually code:
#views.py
from .tables import HistoryTable
class HistoryView(LoginRequiredMixin, SingleTableView):
template_name = "funkwerkstatt/history.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["table"] = Device.history.filter(id=self.kwargs["pk"])
## futur code
#context["table"] = HistoryTable(Device.history.filter(id=self.kwargs["pk"]))
return context
#tables.py
import django_tables2 as tables
class HistoryTable(tables.Table):
device = tables.Column(accessor="device", linkify=True)
class Meta:
model = # HistoryModel?!
empty_text = "No entry"
exclude = ["id",]
Is there a way to referent the auto generated HistoryModel
Reading the docs helps some times.
https://django-simple-history.readthedocs.io/en/latest/common_issues.html#pointing-to-the-model
class PollHistoryListView(ListView): # or PollHistorySerializer(ModelSerializer):
class Meta:
model = Poll.history.model
# ...
Related
Is there any good way that I can make modifications to individual fields of data in a list view? For example, I have a blog post model that looks like
class Blog(models.Model):
create_time = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=250)
document = models.TextField(blank=True)
and my list view looks like this:
class BlogPostListView(ListView):
model = Blog
template_name = 'blog/posts.html'
context_object_name = 'posts'
ordering = ['-create_time']
Obviously I want to display a list of blog posts in a table format, and I want to modify how the create_time looks (currently showing 'March 7, 2020, 10:16 a.m.', but I only want 2020-3-7 10:16); and the post document can be too long to display in a cell, I want to truncate it into only 150 words.
so what are the better ways to achieve this in a listview view?
(I read some sort-of similar questions in StackOverflow, but it is either done in the template such as reformating the time which does not solve my second requirement, or too vague to understand).
If you want to customise the representation between model and view. A faster way to do that is to use serializer and RetrieveAPIView of DRF. Check out installation link. Then, you can make the use of serializer to customise representations of model's fields.
Here is pseudo code:
from rest_framework import serializers
from rest_framework.generics import RetrieveAPIView
class BlogSerializer(serializers.ModelSerializer)
def get_create_time(self, blog):
return self.create_time..strftime("%m/%d/%Y, %H:%M)
def get_document(self, blog):
return self.document[:151]
class BlogPostListView(RetrieveAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
template_name = 'blog/posts.html'
As Chouvic suggested that I should use ModelSerializer, I managed to get it work in the listView. Here is my solution:
serializer:
class BlogListSerializer(serializers.ModelSerializer):
create_time_str = serializers.SerializerMethodField()
short_document_str = serializers.SerializerMethodField()
class Meta:
model = Blog
fields = '__all__'
def get_create_time_str(self, obj):
return obj.create_time.strftime("%Y/%m/%d")
def get_short_document_str(self, obj):
return obj.document[0:150]
as for the ListView:
class BlogListView(ListView):
model = Blog
template_name = 'blog/posts.html'
context_object_name = 'posts'
ordering = ['-create_time']
queryset = Blog.objects.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
posts = context['posts']
mod_posts = BlogListSerializer(posts, many=True)
context['posts'] = mod_posts.data
return context
BlogListSerializer(posts, many=True) takes in posts (a list of post) and return a new list of post each with the new 'attributes' of create_time_str and short_document_str.
In the template, I just need to refer it as <td>{{ post.create_time_str }}</td>.
For example, I have two models:
class Page(models.Model):
# Some fields
...
#property
def title(self):
return PageTranslation.objects.get(page=self, language=language).title # I can not pass property to the parameter
class PageTranslation(models.Model):
page = models.ForeignKey(Page)
title = models.CharField()
And some DRF view, which get_queryset method looks like this:
def get_queryset(self):
return Page.objects.all()
And serializer:
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = (..., 'title',) # title = property
I want to return QuerySet with Page model instances, and use title property in serializer, but I can not pass language (that is set somewhere in the request — headers, query param, etc) there.
What is the correct way to do this?
from django.utils.translation import get_language
from django.config import settings
class Page(models.Model):
#property
def title(self):
language = get_language() or settings.LANGUAGE_CODE
return PageTranslation.objects.get(page=self, language=language).title
get_language() gives you the current active language, if i18n is disabled it gives you None, and for that we have the settings.LANGUAGE_CODE fallback.
For the serializer part, I think you are supposed to explicitly say that your property is a field, ModelSerializer only finds the actual database fields for you, nothing else.
class PageSerializer(serializers.ModelSerializer):
title = serializers.Field()
class Meta:
model = Page
fields = (..., 'title',)
Register django.middleware.locale.LocaleMiddleware in your project settings. This makes LANGUAGE_CODE property available in the request
In DRF view where there is the request context, filter QuerySet by the language.
def get_queryset(self):
return Page.objects.filter(language=self.request.LANGUAGE_CODE)
Then computed title property declared in the Page model is not necessary and inefficient.
This is why:
N queries are executed per record in the original QuerySet to serialise a value for the title field. There is great likelihood for an N+1 problem to occur when another model has a many-to-many relation with Page.
Also, serialised results can be inconsistent because title value can be null in cases where the record doesn't exist for the language.
Lets say that we have two models: ModelA and ModelB.
I will use Django-Tables2 to create a table out of these models.
In tables.py you could have two separate table classes (below).
from .models import ModelA, ModelB
import django_tables2 as tables
class ModelATable(tables.Table):
class Meta:
#some basic parameters
model = ModelA
#the template we want to use
template_name = 'django_tables2/bootstrap.html'
class ModelBTable(tables.Table):
class Meta:
#some basic parameters
model = ModelB
#the template we want to use
template_name = 'django_tables2/bootstrap.html'
This means there will be a table for each model. However I think a more efficient coding solution would be to something as follows.
class MasterTable(tables.Table, request):
#where request is the HTML request
letter = request.user.letter
class Meta:
#getting the correct model by doing some variable formatting
temp_model = globals()[f'Model{letter}']
#some basic parameters
model = temp_model
#the template we want to use
template_name = 'django_tables2/bootstrap.html'
The issue involves passing the request object in the table definition from views.py. It would look something like:
def test_view(request):
#table decleration with the request object passed through...
table = MasterTable(ModelOutput.objects.all(), request)
RequestConfig(request).configure(table)
return render(request, 'some_html.html', {'table': table})
I do not know how to pass through a variable, in this case the request object, to the class so that variable formatting can be done.
I think you are looking for table_factory. This returns a Table class for you which you can use. (Also note, django.apps.apps.get_model is a better way of looking up a model than using globals.)
from django_tables2 import tables
from django.apps import apps
class BaseTable(tables.Table):
class Meta:
template_name = 'django_tables2/bootstrap.html'
def test_view(request):
temp_model = apps.get_model('myapp', f'Model{request.user.letter}')
MasterTable = tables.table_factory(temp_model, table=BaseTable)
table = MasterTable(ModelOutput.objects.all())
RequestConfig(request).configure(table)
return render(request, 'some_html.html', {'table': table})
In my ListView I would like to filter the data by the current user logged from the context_data in :
views.py
class DashboardListView(LoginRequiredMixin,ListView):
model = Links
template_name = 'dashboard/home.html'
context_object_name ='links_list'
paginate_by = 15
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['dashboard_list']= Dashboard.objects.filter()[:15]
context['todo_list']= Todo.objects.all().order_by('-pk')[:15]
context['todo_complete']= Todo.objects.all().count()
context['PasswordUsername_list']= PasswordUsername.objects.all()
return context
I tried to override with a query_set but it does work only for the links model
Well, you need to filter all those queries.
context['dashboard_list']= Dashboard.objects.filter(user=self.request.user)[:15]
context['todo_list']= Todo.objects.filter(user=self.request.user).order_by('-pk')[:15]
etc - assuming your models all have a user FK field pointing to the User model.
See the answer below it's working but do not forget to add :
def get_queryset(self):
return self.model.objects.filter(user=self.request.user)
To your ListView otherwise the first model will still be shown.
I have a model registered on the admin site. One of its fields is a long string expression. I'd like to add custom form fields to the add/update pages of this model in the admin. Based on the values of these fields I will build the long string expression and save it in the relevant model field.
How can I do this?
I'm building a mathematical or string expression from symbols. The user chooses symbols (these are the custom fields that are not part of the model) and when they click save then I create a string expression representation from the list of symbols and store it in the DB. I don't want the symbols to be part of the model and DB, only the final expression.
Either in your admin.py or in a separate forms.py you can add a ModelForm class and then declare your extra fields inside that as you normally would. I've also given an example of how you might use these values in form.save():
from django import forms
from yourapp.models import YourModel
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# ...do something with extra_field here...
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
To have the extra fields appearing in the admin just:
Edit your admin.py and set the form property to refer to the form you created above.
Include your new fields in your fields or fieldsets declaration.
Like this:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
UPDATE:
In Django 1.8 you need to add fields = '__all__' to the metaclass of YourModelForm.
It it possible to do in the admin, but there is not a very straightforward way to it. Also, I would like to advice to keep most business logic in your models, so you won't be dependent on the Django Admin.
Maybe it would be easier (and maybe even better) if you have the two seperate fields on your model. Then add a method on your model that combines them.
For example:
class MyModel(models.model):
field1 = models.CharField(max_length=10)
field2 = models.CharField(max_length=10)
def combined_fields(self):
return '{} {}'.format(self.field1, self.field2)
Then in the admin you can add the combined_fields() as a readonly field:
class MyModelAdmin(models.ModelAdmin):
list_display = ('field1', 'field2', 'combined_fields')
readonly_fields = ('combined_fields',)
def combined_fields(self, obj):
return obj.combined_fields()
If you want to store the combined_fields in the database you could also save it when you save the model:
def save(self, *args, **kwargs):
self.field3 = self.combined_fields()
super(MyModel, self).save(*args, **kwargs)
Django 2.1.1
The primary answer got me halfway to answering my question. It did not help me save the result to a field in my actual model. In my case I wanted a textfield that a user could enter data into, then when a save occurred the data would be processed and the result put into a field in the model and saved. While the original answer showed how to get the value from the extra field, it did not show how to save it back to the model at least in Django 2.1.1
This takes the value from an unbound custom field, processes, and saves it into my real description field:
class WidgetForm(forms.ModelForm):
extra_field = forms.CharField(required=False)
def processData(self, input):
# example of error handling
if False:
raise forms.ValidationError('Processing failed!')
return input + " has been processed"
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# self.description = "my result" note that this does not work
# Get the form instance so I can write to its fields
instance = super(WidgetForm, self).save(commit=commit)
# this writes the processed data to the description field
instance.description = self.processData(extra_field)
if commit:
instance.save()
return instance
class Meta:
model = Widget
fields = "__all__"
You can always create new admin template, and do what you need in your admin_view (override the admin add URL to your admin_view):
url(r'^admin/mymodel/mymodel/add/$','admin_views.add_my_special_model')
If you absolutely only want to store the combined field on the model and not the two seperate fields, you could do something like this:
Create a custom form using the form attribute on your ModelAdmin. ModelAdmin.form
Parse the custom fields in the save_formset method on your ModelAdmin. ModelAdmin.save_model(request, obj, form, change)
I never done something like this so I'm not completely sure how it will work out.
The first (highest score) solution (https://stackoverflow.com/a/23337009/10843740) was accurate, but I have more.
If you declare fields by code, that solution works perfectly, but what if you want to build those dynamically?
In this case, creating fields in the __init__ function for the ModelForm won't work. You will need to pass a custom metaclass and override the declared_fields in the __new__ function!
Here is a sample:
class YourCustomMetaClass(forms.models.ModelFormMetaclass):
"""
For dynamically creating fields in ModelForm to be shown on the admin panel,
you must override the `declared_fields` property of the metaclass.
"""
def __new__(mcs, name, bases, attrs):
new_class = super(NamedTimingMetaClass, mcs).__new__(
mcs, name, bases, attrs)
# Adding fields dynamically.
new_class.declared_fields.update(...)
return new_class
# don't forget to pass the metaclass
class YourModelForm(forms.ModelForm, metaclass=YourCustomMetaClass):
"""
`metaclass=YourCustomMetaClass` is where the magic happens!
"""
# delcare static fields here
class Meta:
model = YourModel
fields = '__all__'
This is what I did to add the custom form field "extra_field" which is not the part of the model "MyModel" as shown below:
# "admin.py"
from django.contrib import admin
from django import forms
from .models import MyModel
class MyModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# Do something with extra_field here
return super().save(commit=commit)
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
You might get help from my answer at :
my response previous on multicheckchoice custom field
You can also extend multiple forms having different custom fields and then assigning them to your inlines class like stackedinline or tabularinline:
form =
This way you can avoid formset complication where you need to add multiple custom fields from multiple models.
so your modeladmin looks like:
inlines = [form1inline, form2inline,...]
In my previous response to the link here, you will find init and save methods.
init will load when you view the page and save will send it to database.
in these two methods you can do your logic to add strings and then save thereafter view it back in Django admin change_form or change_list depending where you want.
list_display will show your fields on change_list.
Let me know if it helps ...
....
class CohortDetailInline3(admin.StackedInline):
model = CohortDetails
form = DisabilityTypesForm
...
class CohortDetailInline2(admin.StackedInline):
model = CohortDetails
form = StudentRPLForm
...
...
#admin.register(Cohort)
class CohortAdmin(admin.ModelAdmin):
form = CityInlineForm
inlines = [uploadInline, cohortDetailInline1,
CohortDetailInline2, CohortDetailInline3]
list_select_related = True
list_display = ['rto_student_code', 'first_name', 'family_name',]
...