When I inherit from admin.ModelAdmin, in history on admin page I can see what fields has been changed. However, now I need to use django-simple-history to track all my model changes. Now, for admin, I inherit for simple_history.SimpleHistoryAdmin. Whilst I can see all of the model changes and revert them, I cannot see, which fields were changed. Is it possible to add that handy functionality to SimpleHistoryAdmin?
I found a way to solve this issue. I added a ModelAdmin method and used History Diffing to add a custom field in the Change history table.
history_list_display = ['changed_fields']
def changed_fields(self, obj):
if obj.prev_record:
delta = obj.diff_against(obj.prev_record)
return delta.changed_fields
return None
What you need is history_list_display field in your Admin. The list of fields included in the history_list_display will be displayed in the history page with their corresponding entries.
Something like this:
class SomeAdmin(admin.ModelAdmin):
def some_user_defined(self, obj):
return "something"
date_hierarchy = 'created_at'
search_fields = ['field1', 'field2']
list_display = ('field1', 'field2',)
list_filter = ('field1',)
history_list_display = ('field1', 'field2', 'some_user_defined',)
This will display field1, field2 along with comment, user and reason
You probably want to do something like that:
# admin.py
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import Website
from django.utils.html import format_html
class WebsiteHistoryAdmin(SimpleHistoryAdmin):
history_list_display = ["changed_fields","list_changes"]
def changed_fields(self, obj):
if obj.prev_record:
delta = obj.diff_against(obj.prev_record)
return delta.changed_fields
return None
def list_changes(self, obj):
fields = ""
if obj.prev_record:
delta = obj.diff_against(obj.prev_record)
for change in delta.changes:
fields += str("<strong>{}</strong> changed from <span style='background-color:#ffb5ad'>{}</span> to <span style='background-color:#b3f7ab'>{}</span> . <br/>".format(change.field, change.old, change.new))
return format_html(fields)
return None
admin.site.register(Website, WebsiteHistoryAdmin)
And you get this as a result:
And if you want to view not only names of changed fields as per Rafi comment and also changed values, next code will do it:
def changed_fields_with_values(self, obj):
fields = ""
if obj.prev_record:
delta = obj.diff_against(obj.prev_record)
for change in delta.changes:
fields += str("{} changed from {} to {}".format(change.field, change.old, change.new))
return fields
return None
Similar to the previous solution from Rafi but using array to list more elegantly the record changes:
def list_changes(self, obj):
diff = []
if obj.prev_record:
delta = obj.diff_against(obj.prev_record)
for change in delta.changes:
diff.append("<b>* {}:</b> changed from `{}` to `{}`".format(change.field, change.old, change.new))
return mark_safe("\n<br>".join(diff))
Related
I need to override variables (or pass dynamic data) to imported class.
filters.py
import django_filters
from .models import Gate, Tram, OperationArea, Bogie
from distutils.util import strtobool
from django import forms
class GateFilter(django_filters.FilterSet):
# Prepare dynamic lists with choices
tram_list = [(id, number) for id, number in Tram.objects.all().values_list('id', 'number')]
bogie_list = [(id, number) for id, number in Bogie.objects.all().values_list('id', 'number')]
area_list = [(id, area) for id, area in OperationArea.objects.all().values_list('id', 'area')]
# Generate fields
tram = django_filters.MultipleChoiceFilter(choices=tram_list, label=u'Tramwaj')
car = django_filters.MultipleChoiceFilter(choices=Gate.CAR_SYMBOLS, label=u'Człon')
bogie = django_filters.MultipleChoiceFilter(choices=bogie_list, label=u'Wózek')
bogie_type = django_filters.MultipleChoiceFilter(choices=Gate.BOGIE_TYPES, label=u'Typ wózka')
area = django_filters.MultipleChoiceFilter(choices=area_list, label=u'Obszar')
operation_no = django_filters.CharFilter(label=u'Numer operacji', widget=forms.TextInput(attrs={'size': '16px'}))
status = django_filters.MultipleChoiceFilter(choices=Gate.GATE_STATUSES, label=u'Status')
rating = django_filters.MultipleChoiceFilter(choices=Gate.GATE_GRADES, label=u'Ocena')
class Meta:
pass
views.py
from .filters import GateFilter
class GateListView(generic.ListView):
queryset = None
gate_type = None
template_name = 'qapp/gate/list.html'
context_object_name = 'gate_list'
paginate_by = 20
def get_queryset(self):
# Type is stored in database as big-letter word, so 'bjc' != 'BJC'.
if self.gate_type.upper() == 'BJW':
ordering = ['bogie', 'bogie_type']
else:
ordering = ['tram', 'car']
queryset = Gate.objects.filter(type=self.gate_type.upper()).order_by(*ordering)
self.gate_list = GateFilter(self.request.GET, queryset=queryset)
return self.gate_list.qs.distinct()
def get_context_data(self, **kwargs):
context = super(GateListView, self).get_context_data(**kwargs)
# Return Gate.type to template.
context['gate_type'] = self.gate_type
# Return object (for generating form) to template.
context['gate_list_filter'] = self.gate_list
return context
As you can see, in the filters.py, the data for variables tram_list, bogie_list and area_list are dynamic (fetched from database).
But during importing this class to views.py, this data becomes static.
I tried to override this values:
using #classmethod decorator in class GateFilter, and calling it
before setting self.gate_list object,
in views.py using GateFilter.tram_list (and the rest) notation,
No luck.
I can't use reload() function, due to import type (from .filters import GateFilter).
Currently for update lists in filters.py I need to rerun whole app.
This is unacceptable for business logic of my app.
This is the wrong approach. Rather, you should be using the filters that are aware of querysets and that evaluate them when required: ModelChoiceFilter and ModelMultipleChoiceFilter.
class GateFilter(django_filters.FilterSet):
team = django_filters.ModelMultipleChoiceFilter(queryset=Tram.objects.all())
i have field('image_tag') from Inlinemodel that i want to display in one row of Orderdetail model.
class SampleImagesInline(admin.StackedInline):
fields = ['image_tag']
readonly_fields = ['image_tag']
model = SampleImages
extra = 0
#admin.register(OrderDetail)
class OrderDetailAdmin(admin.ModelAdmin):
inlines = [SampleImagesInline]
by default these are showing vertically. how to display in one row?.
You can use TabularInline. Try like this:
class SampleImagesInline(admin.TabularInline):
fields = ['image_tag']
readonly_fields = ['image_tag']
model = SampleImages
extra = 0
Update
I think I misunderstood your problem. IMHO, you should not use the InLineAdmin. Instead, try like this:
from django.utils.safestring import mark_safe
...
class OrderDetailAdmin(admin.ModelAdmin):
...
readonly_fields = ['image_tags',]
def image_tags(self, obj):
img_html = ""
for image in obj.image_set.all(): # <-- get related images
img_html += "<img src={}> ".format(image.image.url)
same_line_html = '<div class="tabular inline-related last-related">{}</div>'.format(img_html)
return mark_safe(same_line_html)
image_tags.description = "Images"
Please see here in docs for more information on getting related objects
I can't sort table by it's models property. I know that I should set accessor in the column so django-tables2 knows what field to process but it does not work.
This is the table:
class ScansTable(tables.Table):
site = tables.columns.Column(accessor='occurence.site', verbose_name='Site')
url = tables.columns.TemplateColumn("""{{ record.occurence.url|truncatechars:20 }}""",
accessor='occurence.url', verbose_name='Url')
price = tables.columns.TemplateColumn(u"""{{ record.price }} €""")
date = tables.columns.Column(accessor='date',order_by='date')
time = tables.columns.Column(accessor='time',order_by='time')
class Meta:
model = Scan
fields = ('date', 'time', 'site', 'url', 'valid', 'price')
attrs = {'id': 'cans_table',
'class': 'table',}
This is the Scan model:
class Scan(models.Model):
occurence = models.ForeignKey('Occurence', related_name='scans')
datetime = models.DateTimeField()
price = models.DecimalField(max_digits=20,decimal_places=2,null=True,blank=True,verbose_name='Price')
valid = models.BooleanField(default=True,verbose_name='Valid')
def __unicode__(self):
return u'{} {} {} {}'.format(self.occurence, self.datetime, self.price, u'OK' if self.valid else 'NOK')
#property
def date(self):
return self.datetime.date()
#property
def time(self):
return self.datetime.time()
The view:
def scans(request):
...
scans = Scan.objects.filter(occurence__product=product)
scans_table = ScansTable(scans)
RequestConfig(request).configure(scans_table)
scans_table.paginate(page=request.GET.get('page', 1), per_page=50)
return render(request,"dashboard_app/scans.html",context={'scans_table':scans_table})
The table is being properly renderd when I don't want to sort it. When I click on time (for example), it returns:
Cannot resolve keyword u'time' into field. Choices are: datetime,
groups, id, occurence, occurence_id, price, valid
Do you know where is the problem?
it's strange what the type product ?? you show the Occurence model and what value it in the view
It appears that defined properties/methods of the model are not available for sorting/filtering within the queryset. I don't fully understand why that is the case. A solution would be to NOT define date and time as properties on the Scan model, but instead annotate them to the queryset used to populate the data.
from django.db import models
def scans(request):
...
scans = Scan.objects.filter(occurence__product=product).annotate(
date=models.F('datetime__date'),
time=models.F('datetime__time')
)
...
See the documentation here on field lookups. Also you could use the tables specific columns for those fields - note that you don't need to define the accessors now the results are already in the queryset:
class ScansTable(tables.Table):
...
date = tables.DateColumn()
time = tables.TimeColumn()
...
Unfortunatelly Django doesn't have super-magic Drupal's analog for Views module https://www.drupal.org/project/views (by the way other cms also doesn't have it) so we all need write views in code and add content filters like everyone see in Django Admin by hand.
I need to add filters with dropdowns for Charfield and datepopup widget for DateTime field in my class-based-view, i found django-filter for this http://django-filter.readthedocs.org/en/latest/usage.html
But in docs no example how to setup it with CBW, only with function views.
views.py:
class VkwallpostListView(ListView):
model = Vkwallpost
context_object_name = "vk_list"
def get_template_names(self):
return ["vk_list.html"]
def get_context_data(self, **kwargs):
articles = Vkwallpost.objects.order_by("-date_created")[:5]
videos = Fbpagepost.objects.order_by("-date_created")[:5]
items = list(articles) + list(videos)
items.sort(key=lambda i: i.date_created, reverse=True)
return {"vk_fb_list": items[:5]}
def get_queryset(self):
wallposts = Vkwallpost.objects
if 'all_posts' not in self.request.GET:
pass
elif 'all' in self.request.GET:
pass
else:
success = False
criteria = {}
if 'sentiment' in self.request.GET:
criteria['sentiment'] = self.request.GET['sentiment']
print(criteria)
wallposts = wallposts.filter(**criteria)
return wallposts
And i want to easily add this filters:
import django_filters
class VkwallpostFilter(django_filters.FilterSet):
class Meta:
model = Vkwallpost
fields = ['sentiment', 'date_created']
How to achieve this?
Try to use Django Form with ModelChoiceField or ModelMultipleChoiceField.
Its all that you need.
I want to be able to sort by several custom methods in Django Admin. This question provides solution for one method only.
I tried to modify it:
from django.db import models
class CustomerAdmin(admin.ModelAdmin):
list_display = ('number_of_orders','number_of_somevalue') # added field
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
qs = qs.annotate(models.Count('order'))
qs = qs.annotate(models.Count('somevalue')) # added line
return qs
def number_of_orders(self, obj):
return obj.order__count
number_of_orders.admin_order_field = 'order__count'
def number_of_somevalue(self, obj): # added method
return obj.somevalue__count
number_of_somevalue.admin_order_field = 'somevalue__count'
and it works incorrectly. It seems that it multiplies the count values instead of counting them separately.
Example:
I have 2 orders and 2 somevalues, but in the panel I see 4 orders and 4 somevalues.
Adding another method with yet another value makes it 8 (2*2*2).
How can I fix it?
You can try this to sort by many custom methods (Tested):
from django.db.models import Count
class CustomerAdmin(admin.ModelAdmin):
# The list display must contain the functions that calculate values
list_display = ('number_of_orders','number_of_somevalue') # added field
# Overwrite queryset in model admin
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
# The query have to return multiple annotation, for this use distinct=True in the Count function
qs = qs.annotate(number_orders = Count('order', distinct=True)).annotate(number_somevalue = Count('somevalue',distinct=True))
return qs
# This function return the new field calculated in queryset (number_orders)
def number_of_orders(self, obj):
return obj.number_orders
number_of_orders.admin_order_field = 'numberorders' # sortable new column
# And this one will return the another field calculated (number_somevalue)
def number_of_somevalue(self, obj): # added method
return obj.number_somevalue
number_of_somevalue.admin_order_field = 'number_somevalue'# sortable new column