Multiple annotate with sum and display data in admin - Django - python

I'm new to both Django and Python. Currently I'm trying the Django Admin by doing.
I've three models for a Django app, which are GoodsItem, SoldGoodsItem and FinishedGoodsItem. The models.py is:
from django.db import models
class GoodsItem(models.Model):
name = models.CharField(max_length=255)
size = models.DecimalField(max_digits=4, decimal_places=2)
INCHES = 'IN'
NUMBER = 'NUM'
GOODS_ITEM_SIZE_UNITS = (
(INCHES, 'Inches'),
(NUMBER, '#'),
)
size_unit = models.CharField(
max_length=4,
choices=GOODS_ITEM_SIZE_UNITS,
default=INCHES,
)
def __str__(self):
if(self.size_unit == self.NUMBER):
return "%s #%s" % (self.name, (self.size).normalize())
else:
return "%s %s\"" % (self.name, (self.size).normalize())
class FinishedGoodsItem(models.Model):
date = models.DateField()
goods_item = models.ForeignKey(GoodsItem, on_delete=models.CASCADE, related_name="finished_name")
weight = models.DecimalField(max_digits=6, decimal_places=3)
def __str__(self):
return str(self.goods_item)
class SoldGoodsItem(models.Model):
goods_item = models.ForeignKey(GoodsItem, on_delete=models.CASCADE, related_name="sold_name")
date = models.DateField()
weight = models.DecimalField(max_digits=6, decimal_places=3)
def __str__(self):
return str(self.goods_item)
And here is admin.py:
from django.contrib import admin
from django.db.models import Sum
from .models import GoodsItem, FinishedGoodsItem, SoldGoodsItem
#admin.register(SoldGoodsItem)
#admin.register(FinishedGoodsItem)
class FinishedGoodsItemAdmin(admin.ModelAdmin):
fields = ('date', 'goods_item', 'weight')
list_display = ('date', 'goods_item', 'weight')
#admin.register(GoodsItem)
class GoodsItemAdmin(admin.ModelAdmin):
list_display = ('__str__', 'finished_good', 'sold_good', 'stock_available')
def get_queryset(self, request):
qs = super(GoodsItemAdmin, self).get_queryset(request)
qs = qs.annotate(
finished_good=Sum('finished_name__weight'),
sold_good=Sum('sold_name__weight'),
stock_available=Sum('finished_name__weight') - Sum('sold_name__weight'),
)
return qs
def finished_good(self, obj):
return obj.finished_good
def sold_good(self, obj):
return obj.sold_good
def stock_available(self, obj):
return obj.stock_available
In stock_available for each GoodsItem, I want to display the difference between all entries of FinishedGoodsItem and all entries of SoldGoodsItem. For now, I'm getting incorrect value for all three annotated fields which are finished_good, sold_good and stock_available. I'm unable to find the reason for that. In Django Debug Toolbar suggest that duplicate queries are being executed.

This is known issue and occurs when we try to combine multiple aggregation, as mentioned in docs.
As a workaround for this particular problem, we can use Subquery expression. Here is my updated admin.py using Subquery expression in get_queryset method of GoodsItemAdmin.
from django.contrib import admin
from django.db.models import Subquery, Sum, OuterRef
from .models import GoodsItem, FinishedGoodsItem, SoldGoodsItem
#admin.register(SoldGoodsItem)
class SoldGoodsItemAdmin(admin.ModelAdmin):
fields = ('date', 'goods_item', 'weight')
list_display = ('date', 'goods_item', 'weight')
#admin.register(FinishedGoodsItem)
class FinishedGoodsItemAdmin(admin.ModelAdmin):
fields = ('date', 'goods_item', 'weight')
list_display = ('date', 'goods_item', 'weight')
#admin.register(GoodsItem)
class GoodsItemAdmin(admin.ModelAdmin):
list_display = ('__str__', 'finished_good', 'sold_good', 'stock_available')
def get_queryset(self, request):
qs = super(GoodsItemAdmin, self).get_queryset(request)
qs = qs.annotate(
finished_good = Subquery(FinishedGoodsItem.objects.filter(goods_item=OuterRef('pk'))\
.values('goods_item_id').annotate(sum=Sum('weight')).values('sum')[:1]),
sold_good = Subquery(SoldGoodsItem.objects.filter(goods_item=OuterRef('pk'))\
.values('goods_item_id').annotate(sum=Sum('weight')).values('sum')[:1])
)
return qs
def finished_good(self, obj):
return obj.finished_good
def sold_good(self, obj):
return obj.sold_good
def stock_available(self, obj):
finished_good = 0 if self.finished_good(obj) is None else self.finished_good(obj)
sold_good = 0 if self.sold_good(obj) is None else self.sold_good(obj)
return '-' if (finished_good == 0 and sold_good == 0) else finished_good - sold_good
Hope someone finds this useful.

Related

How to pass pk argument within class based view to queryset in Django

I have the following Django urls/views and Models:
Models.py:
ORDER_COLUMN_CHOICES = Choices(
('0', 'id'),
('1', 'code'),
('2', 'code_type'),
('3', 'created'),
('4', 'updated'),
('5', 'valid'),
)
class Identifier(TimeStampMixin, models.Model):
code_type = models.CharField(max_length=10, null=True)
code = models.CharField(max_length=12)
account = models.ForeignKey(Account, on_delete=models.CASCADE, null=True)
actflag = models.CharField(max_length=1, blank=True)
valid = models.BooleanField(default=False)
def __str__(self):
return self.code
class Meta:
db_table = "portfolio_identifier"
def query_identifier_by_args(**kwargs):
draw = int(kwargs.get('draw', None)[0])
length = int(kwargs.get('length', None)[0])
start = int(kwargs.get('start', None)[0])
search_value = kwargs.get('search[value]', None)[0]
order_column = kwargs.get('order[0][column]', None)[0]
order = kwargs.get('order[0][dir]', None)[0]
order_column = ORDER_COLUMN_CHOICES[order_column]
# django orm '-' -> desc
if order == 'desc':
order_column = '-' + order_column
queryset = Identifier.objects.all()
total = queryset.count()
if search_value:
queryset = queryset.filter(Q(id__icontains=search_value) |
Q(code__icontains=search_value) |
Q(code_type__icontains=search_value) |
Q(created__icontains=search_value) |
Q(updated__icontains=search_value) |
Q(valid__icontains=search_value))
count = queryset.count()
queryset = queryset.order_by(order_column)[start:start + length]
return {
'items': queryset,
'count': count,
'total': total,
'draw': draw
}
Urls.py
from . import views
from rest_framework.routers import DefaultRouter
from apps.portfolio.views import IdentifierViewSet
router = DefaultRouter()
router.register(r'portfolio', IdentifierViewSet)
urlpatterns = [
path('portfolios/', views.portfolios, name="portfolios"),
path('portfolio/<str:pk>/', views.portfolio, name="portfolio"),
path('api/', include(router.urls)),
]
Views.py
def portfolio(request, pk):
portfolio = Account.objects.get(id=pk)
identifiers = Identifier.objects.filter(account=pk)
context = {"portfolio": portfolio, "identifiers": identifiers}
return render(request, 'portfolio.html', context)
class IdentifierViewSet(viewsets.ModelViewSet):
queryset = Identifier.objects.all()
serializer_class = IdentifierSerializer
authentication_classes = []
def get_queryset(self):
account_pk = self.kwargs["pk"]
return super().get_queryset().filter(account=account_pk)
def list(self, request, **kwargs):
try:
identifier = query_identifier_by_args(**request.query_params)
serializer = IdentifierSerializer(identifier['items'], many=True)
result = dict()
result['data'] = serializer.data
result['draw'] = identifier['draw']
result['recordsTotal'] = identifier['total']
result['recordsFiltered'] = identifier['count']
return Response(result, status=status.HTTP_200_OK, template_name=None, content_type=None)
except Exception as e:
return Response(e, status=status.HTTP_404_NOT_FOUND, template_name=None, content_type=None)
Within the Views I have a Class based view IdentifierViewSet with the following queryset line queryset = Identifier.objects.all() which retreievs all data from the db model however I would like to only retrieve queryset based on portfolio associated with the user's account which works using the following lines of code taken from the function based view portfolio:
portfolio = Account.objects.get(id=pk)
identifiers = Identifier.objects.filter(account=pk)
I was unable to pass the pk from urls to this class based view but was able to do so with the function based view.
How can I go about passing the above object queries to within the Class based view to replace the queryset in the Class based view queryset = Identifier.objects.all()?
To do that you can override the get_queryset method from the Django ModelViewSet.
def get_queryset(self):
portfolio = Account.objects.get(id=self.request.account.pk)
#... do something
identifiers = Identifier.objects.filter(account=self.request.account.pk)
return identifiers
There are list(), retrieve(), create(), update(), partial_update(), destroy() functions and the pk parameter can be passed to four functions except list and create.
For example, for PUT method of the API, you can customize the update function.
class IdentifierViewSet(viewsets.ModelViewSet):
queryset = Identifier.objects.all()
serializer_class = IdentifierSerializer
authentication_classes = []
def list(self, request, *args, **kwargs):
...
def update(self, request, pk):
# you can get the pk here
print(pk)
# you can get the object with the current `pk`
instance = self.get_object()
...
You can also customize other functions like this.

How can I filter objects in my model to only show those of my ForeignKey user?

Trying to filter objects in my view.py to only show items (in my case Buckets) owned by Users.
I implemented the code below in my original Model [my model.py code is at the bottom of post]
class PostObjects(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status=Bucket.owner)
But I'm not sure if that is the correct procedure to list all items?
Here is the view.py where I'm trying to filter data by User aka owner. Users should ONLY be allowed to view their own items. (I will deal with permissions later)
class BucketList(generics.ListCreateAPIView):
queryset = Bucket.objects.all() #INSERT FILTER HERE
pass
Here is the model I'm referring too.
class Bucket(models.Model):
options = (
('personal', 'Personal'),
('social', 'Social'),
)
class PostObjects(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status=Bucket.owner)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='buckets')
users = models.ManyToManyField(settings.AUTH_USER_MODEL)
category = models.CharField(max_length=30, choices=options)
name = models.CharField(max_length=35)
created = models.DateTimeField(default=timezone.now)
slug = models.SlugField(unique=True, blank=True)
stock_count = models.IntegerField(blank=True, null=True)
stock_list = ArrayField(models.CharField(max_length=6),size=10)
objects = models.Manager()
postobjects = PostObjects()
class Meta:
ordering = ('-created',)
def total_stocks_calc(self):
self.stock_count = Bucket.objects.aggregate(Sum('stock_list', distinct=True))
self.save()
def get_absolute_url(self):
return reverse("bucket:bucket-view", kwargs={"slug": self.slug})
def __str__(self):
return self.stock_list
To re-state my question, how can I filter objects owned by users in class BucketList for their private view only?
UPDATE:
from django.db.models import Q
from rest_framework import generics
from bucket.models import Bucket
from .serializers import BucketSerializer
class OwnerOrUserFilterBackend(filters.BaseFilterBackend):
queryset = Bucket.postobjects.all() # wondering if you didnt write this for brevity reasons or because its not need due to the class?
def filter_queryset(self, request, queryset, view):
return queryset.filter(
Q(owner=request.user) | #do I not need to change one of the filters too request.owner?
Q(users=request.user)
)
class BucketList(generics.ListCreateAPIView):
model = Bucket
filter_backends = [OwnerOrUserFilterBackend]
You can override the get_queryset method and filter with self.request.user:
class BucketList(generics.ListCreateAPIView):
model = Bucket
def get_queryset(self, *args, **kwargs):
return super.get_queryset(*args, **kwargs).filter(
owner=self.request.user
)
For an API view, it probably is however better to define a filter, and then use this over all the API views where you want to apply this:
class IsOwnerFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.filter(owner=request.user)
Then you can use this in your ListCreateAPIView with:
class BucketList(generics.ListCreateAPIView):
model = Bucket
filter_backends = [IsOwnerFilterBackend]
The advantage of this is that if you have other views that require the same filtering, you only need to add the IsOwnerFilterBackend as filter_backend.
Another filter could include both the owner and the users as people who can see the BucketList:
from django.db.models import Q
class OwnerOrUserFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.filter(
Q(owner=request.user) |
Q(users=request.user)
)
then we thus filter with this filter:
class BucketList(generics.ListCreateAPIView):
model = Bucket
filter_backends = [OwnerOrUserFilterBackend]

Django: inlineformsets

I am trying to incorporate inline add to my django form. The User can create a 'Site', within the 'Site' form the user can create multible 'Staff' to that 'Site'.
I have followed a tutorial which I believe to be the solution but can not get it to work.
Currently I am getting the error:
'Calling modelformset_factory without defining 'fields' or 'exclude' explicitly is prohibited.'
Here is my attempt.
models.py
class Site(models.Model):
...
class Staff(models.Model):
site = models.ForeignKey(Site)
....
views.py
class BaseNestedFormset(BaseInlineFormSet):
def add_fields(self, form, index):
# allow the super class to create the fields as usual
super(BaseNestedFormset, self).add_fields(form, index)
form.nested = self.nested_formset_class(
instance=form.instance,
data=form.data if self.is_bound else None,
prefix=' %s-%s' % (
form.prefix,
self.nested_formset_class.get_default_prefix(),
),
)
def is_valid(self):
result = super(BaseNestedFormset, self).is_valid()
if self.is_bound:
# look at any nested formsets, as well
for form in self.forms:
result = result and form.nested.is_valid()
return result
def save(self, commit=True):
result = super(BaseNestedFormset, self).save(commit=commit)
for form in self:
form.nested.save(commit=commit)
return result
def nested_formset_factory(site_model, staff_model):
parent_child = inlineformset_factory(
site_model,
staff_model,
formset=BaseNestedFormset,
)
parent_child.nested_formset_class = inlineformset_factory(
staff_model,
)
return parent_child
class SiteCreate(CreateView):
model = Site
form_class = SiteForm
queryset = Site.objects.all()
success_url = '/site/list'
def get_form_class(self):
return nested_formset_factory(
Site,
Staff,
)
forms.py
class SiteForm(forms.ModelForm):
class Meta:
model = Site
exclude = ('creation', 'last_modified')
def nested_formset_factory(site_model, staff_model):
parent_child = inlineformset_factory(
site_model,
staff_model,
formset=BaseNestedFormset,
fields = ('one', 'two', 'ect')
)

django admin - how to implement a custom field sort

Let's say I have a data model like so:
Apple->Fruit->Organic
so Apple has a foreign key to Fruit...
and Organic has 3 fields: name, title, level.
I'm displaying the 3 fields in a single column as name_title_level.
I want to be able to sort the result by clicking on the table header.
I've looked at:
https://djangosnippets.org/snippets/2110/
and tried it:
class SpecialOrderingChangeList(ChangeList):
def apply_special_ordering(self, queryset):
order_type, order_by = [self.params.get(param, None) for param in ('ot', 'o')]
special_ordering = self.model_admin.special_ordering
if special_ordering and order_type and order_by:
try:
order_field = self.list_display[int(order_by)]
ordering = special_ordering[order_field]
if order_type == 'desc':
ordering = ['-' + field for field in ordering]
queryset = queryset.order_by(*ordering)
except IndexError:
return queryset
except KeyError:
return queryset
return queryset
def get_query_set(self):
queryset = super(SpecialOrderingChangeList, self).get_query_set()
queryset = self.apply_special_ordering(queryset)
return queryset
#admin.register(Apple)
class AppleAdmin(admin.ModelAdmin):
list_display = ('x', 'get_name')
def get_name(self, obj):
return "{}_{}_{}".format(obj.fruit.organic.name,\
obj.fruit.organic.title, obj.fruit.organic.level)
special_ordering = {'name': ('fruit__organic__name', 'fruit__organic__title', 'fruit__organic__level')}
def get_changelist(self, request, **kwargs):
return SpecialOrderingChangeList
I'm not getting any error and the sort feature is not doing anything.
The get_query_set method is not being called.
Does anyone know how to do this?
Updates:
Here is the updated code. Now the method gets called but still there is no sorting functionality. I mean there is no link in the header at all.
models.py:
from django.db import models
class Organic(models.Model):
name = models.CharField(max_length=30)
title = models.CharField(max_length=30)
label = models.CharField(max_length=30)
def __unicode__(self):
return self.name
class Fruit(models.Model):
organic = models.ForeignKey(Organic)
def __unicode__(self):
return self.organic.name
class Apple(models.Model):
fruit = models.ForeignKey(Fruit)
color = models.CharField(max_length=30)
def __unicode__(self):
return self.color
admin.py:
from django.contrib import admin
from .models import *
from django.contrib.admin.views.main import ChangeList
#admin.register(Organic)
class OrganicAdmin(admin.ModelAdmin):
pass
#admin.register(Fruit)
class FruitAdmin(admin.ModelAdmin):
pass
class SpecialOrderingChangeList(ChangeList):
def apply_special_ordering(self, queryset):
order_type, order_by = [self.params.get(param, None) for param in ('ot', 'o')]
special_ordering = self.model_admin.special_ordering
if special_ordering and order_type and order_by:
try:
order_field = self.list_display[int(order_by)]
ordering = special_ordering[order_field]
if order_type == 'desc':
ordering = ['-' + field for field in ordering]
queryset = queryset.order_by(*ordering)
except IndexError:
return queryset
except KeyError:
return queryset
return queryset
def get_queryset(self, request):
queryset = super(SpecialOrderingChangeList, self).get_queryset(request)
queryset = self.apply_special_ordering(queryset)
return queryset
#admin.register(Apple)
class AppleAdmin(admin.ModelAdmin):
list_display = ('color', 'get_name')
def get_name(self, obj):
return "{}_{}_{}".format(obj.fruit.organic.name,\
obj.fruit.organic.title, obj.fruit.organic.label)
special_ordering = {'name': ('fruit__organic__name', 'fruit__organic__title', 'fruit__organic__label')}
def get_changelist(self, request, **kwargs):
return SpecialOrderingChangeList
it should be
get_queryset()
not
get_query_set()

Default value for ForeignKey field in django admin panel

I have two django Models: PageModel and RecordModel. They are registered in django admin panel. I want automatically create RecordModel object and assign it to (object of PageModel).record field if record is not selected in process of Page creation. (Django Admin - Add Page form) I tried to create form and used clean_record() method, but that not work. (debugger is not stopped there) How can i solve the problem?
Models (Sortable and SortableAdmin classes are part of adminsortable (https://github.com/iambrandontaylor/django-admin-sortable), but I think it does not really matter):
class Record(Sortable):
"""
Запись в книге почетных гостей
"""
class Meta(Sortable.Meta):
verbose_name = u'Запись'
verbose_name_plural = u'Записи'
author = models.CharField(verbose_name=u'Автор', max_length=255, unique=False, blank=False,
default=author_default)
def __unicode__(self):
return u'Запись {} ({})'.format(self.id, self.author)
class Page(Sortable):
"""
Страница книги почетных гостей
"""
class Meta(Sortable.Meta):
verbose_name = u'Страница'
verbose_name_plural = u'Страницы'
record = SortableForeignKey(Record, verbose_name=u'Запись', related_name='pages', blank=False, default=None)
image = models.ImageField(verbose_name=u'Картинка',
upload_to='pages',
default='',
help_text=u'Размер файла - до 10 MB. Формат PNG.',
validators=[ImageValidator(formats=['PNG'], max_size=10000000)])
updated = models.DateTimeField(verbose_name=u'Обновление', auto_now=True, null=True,
help_text=u'Время последнего изменения страницы на сервере')
def __unicode__(self):
return u'Страница {} ({})'.format(self.id, self.image)
Admin:
class PageInline(SortableTabularInline):
model = Page
#admin.register(Record)
class RecordAdmin(SortableAdmin):
list_display = ['author', 'pages_count']
inlines = [PageInline]
fields = ['author']
def pages_count(self, object):
return object.pages.count()
pages_count.short_description = u'Количество страниц'
pages_count.allow_tags = False
class PageAdminForm(forms.ModelForm):
def clean_record(self):
return self.cleaned_data["record"]
#admin.register(Page)
class PageAdmin(SortableAdmin):
list_display = ['__unicode__', 'image', 'author', 'updated']
form = PageAdminForm
readonly_fields = ['updated']
def get_fields(self, request, obj=None):
if obj:
return super(PageAdmin, self).get_fields(request, obj)
else:
return ['record', 'image']
def author(self, page):
return page.record.author
author.allow_tags = False
author.short_description = u'Автор записи'
I solved the problem as follows:
Change record field in Page model
record = SortableForeignKey(Record, verbose_name=u'Запись', related_name='pages', null=True, blank=True, default=None)
Add save_model() method to PageAdmin
def save_model(self, request, obj, form, change):
if obj.record is None:
record = Record.objects.create(author=BookConfiguration.get_solo().record_author_default + ' (' + timezone.localtime(timezone.now()).strftime('%Y-%m-%d %H:%M:%S') + ')')
record.save()

Categories