How to add foreign-key column to dj-stripe admin? - django

I would like to add plan names to dj-stripe django admin so I can see a readable name for what each subscription is associated with. Adding "cancel_at" worked, but I can't use the name of a Product from a Plan.
In my_app\admin.py I do this:
from djstripe.models import Subscription
from djstripe.admin import StripeModelAdmin, SubscriptionItemInline
...
class SubscriptionAdmin(StripeModelAdmin):
list_display = ("plan__product__name", "customer", "status", "cancel_at")
list_filter = ("status", "cancel_at_period_end")
list_select_related = ("customer", "customer__subscriber")
inlines = (SubscriptionItemInline,)
def _cancel(self, request, queryset):
"""Cancel a subscription."""
for subscription in queryset:
subscription.cancel()
_cancel.short_description = "Cancel selected subscriptions" # type: ignore # noqa
actions = (_cancel,)
admin.site.unregister(Subscription)
admin.site.register(Subscription, SubscriptionAdmin)
...
Which produces this error:
Subscription has no field named 'plan__product__name'
How do I add extra columns in dj-stripe that require foreign key lookups?

One solution is to make a callable then reference it in the modeladmin class.
Per the docs:
ModelAdmin.list_display
Set list_display to control which fields are displayed on the change list page of the admin.
There are four types of values that can be used in list_display. All but the simplest may use the display() decorator is used to
customize how the field is presented:
A callable that accepts one
argument, the model instance. For example:
#admin.display(description='Name')
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
class PersonAdmin(admin.ModelAdmin):
list_display = (upper_case_name,)
Which means in my case I can do this to add a combined tier + interval column:
#admin.display(description='Subscription Tier and Interval')
def subscription_tier_interval(obj):
return ("%s - %s" % (obj.plan.product.name, obj.plan.interval))
class SubscriptionAdmin(StripeModelAdmin):
list_display = ("customer", "status", subscription_tier_interval, "cancel_at")
list_filter = ("status", "cancel_at_period_end")
list_select_related = ("customer", "customer__subscriber")
inlines = (SubscriptionItemInline,)
def _cancel(self, request, queryset):
"""Cancel a subscription."""
for subscription in queryset:
subscription.cancel()
_cancel.short_description = "Cancel selected subscriptions" # type: ignore # noqa
actions = (_cancel,)
admin.site.unregister(Subscription)
admin.site.register(Subscription, SubscriptionAdmin)

Related

Use non model django fields in django filter

I have a variable 'cptCodeTBX' which is not present as fields in django models. I need to apply filter on 'cptCodeTBX' variable. Something roughly equivalent to
cptCodeTBX = '00622'
select * from cpt where cpt.code like cptCodeTBX or cptCodeTBX is != ''
In dot net entity framework we could do it by
b = cptContext.CPTs.AsNoTracking().Where(
a =>
(String.IsNullOrEmpty(cptCodeTBX) || a.Code.StartsWith(cptCodeTBX))
This may not be the most performant solution, but I was able to get it working.
Step 1: Read the Django Filter docs.
https://django-filter.readthedocs.io/en/stable/index.html
Step 2: Add a property to your Django model named cptCodeTBX.
from django.db import models
class MyModel(models.Model):
field = models.CharField(max_length=60)
#property
def cptCodeTBX(self):
"""
Does all the code tbx cpt stuff, can do whatever you want.
"""
cptCodeTBX = 2323 #whatever value you want
return cptCodeTBX
Step 3: Add a Django Filter.
import django_filters
class CptCodeTBXFilter(django_filters.FilterSet):
"""
Filter that will do all the magic, value comes from url query params.
Replace filters.NumberFilter with any filter you want like
filters.RangeFilter.
"""
cptCodeTBX = django_filters.NumberFilter(
field_name="cptCodeTBX",
method="filter_cptCodeTBX",
label="Cpt Code TBX",
)
def filter_cptCodeTBX(self, queryset, name, value):
objects_ids = [
obj.pk for obj in MyModel.objects.all() if obj.cptCodeTBX == value
]
if objects_ids:
queryset = MyModel.objects.filter(pk__in=objects_ids)
else:
queryset = MyModel.objects.none()
return queryset
Step 4: Pass the value through the url as a query parameter.
http://example.com/?cptCodeTBX=00622

How do you query a model for an aggregate of a ForeignKey computed field

How do you query a model for an aggregate of a ForeignKey 'computed' (Autofield?) field
I have two models:
class Job(models.Model):
name = models.CharField(…)
class LineItem(models.Model):
job = models.ForeignKey(Job, …)
metric_1 = models.DecimalField(…)
metric_2 = models.DecimalField(…)
metric_3 = models.DecimalField(…)
# Making this a #property, #cached_property, or the like makes no difference
def metric_total(self):
return (self.metric_1 + self.metric_2 * self.metric_3)
In the view:
class StackoverflowView(ListView, …):
model = Job
def get_queryset(self):
return Job.objects
.select_related(…)
.prefetch_related('lineitem_set')
.filter(…).order_by(…)
def get_context_data(self, **kwargs):
context_data = super(StackoverflowView, self).get_context_data(**kwargs)
context_data['qs_aggregate'] = self.get_queryset() \
.annotate(
# Do I need to Annotate the model?
).aggregate(
# This works for any of the fields that have a model field type
metric1Total=Sum('lineitem__metric_1'),
metric2Total=Sum('lineitem__metric_2'),
# This will error :
# Unsupported lookup 'metric_total' for AutoField or join on the field not permitted.
# How do I aggregate the computed model field 'metric_total'?
metricTotal=Sum('lineitem__metric_total'),
)
return context_data
When I try to aggregate the computed field, I get the error: Unsupported lookup 'metric_total' for AutoField or join on the field not permitted.. How do I aggregate these special fields?
You have to calculate metric_total too
Job.objects.aggregate(
metric1Total=Sum('lineitem__metric_1'),
metric2Total=Sum('lineitem__metric_2'),
metric_total=Sum('lineitem__metric_1') + Sum('lineitem__metric_2')
)
A possible dup: Django #property used in .aggregate()
In short, these annotate() and aggregate() methods are performed on the Database level whereas the metric_total() "method" performs on the Python level.

Optimize the filtering of Queryset results in Django

I'm overriding Django Admin's list_filter (to customize the filter that shows on the right side on the django admin UI for a listview). The following code works, but isn't optimized: it increases SQL queries by "number of product categories".
(The parts to focus on, in the following code sample are, qs.values_list('product_category', flat=True) which only returns an id (int), so I've to use ProductCategory.objects.get(id=i).)
Wondering if this can be simplified?
(E.g. data: Suppose the product categories are "baked" "fried" "raw" etc., and the Items are "bread" "fish fry" "cake". So when the Item list is displayed in Django Admin, all product categories will show on the 'Filter By' column on the right side of the UI.)
from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter
from product_category.model import ProductCategory
class ProductCategoryFilter(SimpleListFilter):
title = _('ProductCategory')
parameter_name = 'product_category'
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
ordered_filter_obj_list = []
# TODO: Works, but increases SQL queries by "number of product categories"
for i in (
qs.values_list("product_category", flat=True)
.distinct()
.order_by("product_category")
):
cat = ProductCategory.objects.get(id=i)
ordered_filter_obj_list.append((i, cat))
return ordered_filter_obj_list
def queryset(self, request, queryset):
if self.value():
return queryset.filter(product_category__exact=self.value())
# P.S. Above filter is used in another class like so
class ItemAdmin(admin.ModelAdmin):
list_filter = (ProductCategoryFilter,)
Probably you are looking for select_related, I do not know your exact models structure, but you may use it as follow:
cats = set()
for p in Product.objects.all().select_related('category'):
# Without select_related(), this would make a database query for each
# loop iteration in order to fetch the related categories for each product.
cats.add(p.category)
I am Assuming there is some relation between your Product and ProductCategory models. Hope this help.
Hah, phrasing the question makes it clear in your own head! Found an answer mins after posting this:
(Instead of doing an objects.get() inside the for loop, we can do objects.all() (which is a single SQL Query) and fill up a temporary dictionary. Then use this temp dict to find the associated string value.)
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
category_list = {}
for x in ProductCategory.objects.all():
category_list[x.id] = str(x)
ordered_filter_obj_list = []
for i in (
qs.values_list("product_category", flat=True)
.distinct().order_by("product_category")
):
ordered_filter_obj_list.append((i, category_list[i]))
return ordered_filter_obj_list
First parameter on the tuple list is the value of the lookup, and the second is just the name for display. This can be done in a single SQL query, or via the Django ORM:
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request).select_related('product_category')
values = qs.values('product_category_id', 'product_category__name') #assuming ProductCategory has an attribute 'name'
unique_categories = values.distinct('product_category_id', 'product_category__name')
categories = []
for c in unique_categories:
categories.append((c['product_category_id'], c['product_category__name']))
return categories

Django - sort records by custom column

I can't figure out how to make admin able to sort records using custom column - hours_to_deadline (when they clicks on the column header). In my case, it's timedelta.
class JobAdmin(SuperModelAdmin):
...
list_display = ['id', 'identificator', 'created', 'invoice__estimated_delivery','hours_to_deadline','customer__username', 'language_from__name', 'language_to__name',
'delivery__status', 'confirmed', 'approved', 'invoice__final_price']
...
def hours_to_deadline(self,obj):
try:
return (obj.invoice.estimated_delivery - now())
except:
return None
I found this solution: https://stackoverflow.com/a/15935591/3371056
But in my case, I can't just do sum or something similar.
Do you know what to do?
You cannot order by a field that is not an actual database field, because all the sorting is done on the database level. If it has a value related somehow to a database field you can do something like that in the model definition:
hours_to_deadline.admin_order_field = 'database_field'
You can read more about it at
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/
Answer is: ordering = ('-id',)
class JobAdmin(SuperModelAdmin):
list_display = ['id', 'identificator', 'created', 'invoice__estimated_delivery','hours_to_deadline','customer__username', 'language_from__name', 'language_to__name',
'delivery__status', 'confirmed', 'approved', 'invoice__final_price']
ordering = ('-id',)
def hours_to_deadline(self,obj):
try:
return (obj.invoice.estimated_delivery - now())
except:
return None

Django unique_together with nullable ForeignKey

I'm using Django 1.8.4 in my dev machine using Sqlite and I have these models:
class ModelA(Model):
field_a = CharField(verbose_name='a', max_length=20)
field_b = CharField(verbose_name='b', max_length=20)
class Meta:
unique_together = ('field_a', 'field_b',)
class ModelB(Model):
field_c = CharField(verbose_name='c', max_length=20)
field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True)
class Meta:
unique_together = ('field_c', 'field_d',)
I've run proper migration and registered them in the Django Admin. So, using the Admin I've done this tests:
I'm able to create ModelA records and Django prohibits me from creating duplicate records - as expected!
I'm not able to create identical ModelB records when field_b is not empty
But, I'm able to create identical ModelB records, when using field_d as empty
My question is: How do I apply unique_together for nullable ForeignKey?
The most recent answer I found for this problem has 5 year... I do think Django have evolved and the issue may not be the same.
Django 2.2 added a new constraints API which makes addressing this case much easier within the database.
You will need two constraints:
The existing tuple constraint; and
The remaining keys minus the nullable key, with a condition
If you have multiple nullable fields, I guess you will need to handle the permutations.
Here's an example with a thruple of fields that must be all unique, where only one NULL is permitted:
from django.db import models
from django.db.models import Q
from django.db.models.constraints import UniqueConstraint
class Badger(models.Model):
required = models.ForeignKey(Required, ...)
optional = models.ForeignKey(Optional, null=True, ...)
key = models.CharField(db_index=True, ...)
class Meta:
constraints = [
UniqueConstraint(fields=['required', 'optional', 'key'],
name='unique_with_optional'),
UniqueConstraint(fields=['required', 'key'],
condition=Q(optional=None),
name='unique_without_optional'),
]
UPDATE: previous version of my answer was functional but had bad design, this one takes in account some of the comments and other answers.
In SQL NULL does not equal NULL. This means if you have two objects where field_d == None and field_c == "somestring" they are not equal, so you can create both.
You can override Model.clean to add your check:
class ModelB(Model):
#...
def validate_unique(self, exclude=None):
if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \
field_d__isnull=True).exists():
raise ValidationError("Duplicate ModelB")
super(ModelB, self).validate_unique(exclude)
If used outside of forms you have to call full_clean or validate_unique.
Take care to handle the race condition though.
#ivan, I don't think that there's a simple way for django to manage this situation. You need to think of all creation and update operations that don't always come from a form. Also, you should think of race conditions...
And because you don't force this logic on DB level, it's possible that there actually will be doubled records and you should check it while querying results.
And about your solution, it can be good for form, but I don't expect that save method can raise ValidationError.
If it's possible then it's better to delegate this logic to DB. In this particular case, you can use two partial indexes. There's a similar question on StackOverflow - Create unique constraint with null columns
So you can create Django migration, that adds two partial indexes to your DB
Example:
# Assume that app name is just `example`
CREATE_TWO_PARTIAL_INDEX = """
CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d)
WHERE field_d IS NOT NULL;
CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c)
WHERE field_d IS NULL;
"""
DROP_TWO_PARTIAL_INDEX = """
DROP INDEX model_b_2col_uni_idx;
DROP INDEX model_b_1col_uni_idx;
"""
class Migration(migrations.Migration):
dependencies = [
('example', 'PREVIOUS MIGRATION NAME'),
]
operations = [
migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX)
]
Add a clean method to your model - see below:
def clean(self):
if Variants.objects.filter("""Your filter """).exclude(pk=self.pk).exists():
raise ValidationError("This variation is duplicated.")
I think this is more clear way to do that for Django 1.2+
In forms it will be raised as non_field_error with no 500 error, in other cases, like DRF you have to check this case manual, because it will be 500 error.
But it will always check for unique_together!
class BaseModelExt(models.Model):
is_cleaned = False
def clean(self):
for field_tuple in self._meta.unique_together[:]:
unique_filter = {}
unique_fields = []
null_found = False
for field_name in field_tuple:
field_value = getattr(self, field_name)
if getattr(self, field_name) is None:
unique_filter['%s__isnull' % field_name] = True
null_found = True
else:
unique_filter['%s' % field_name] = field_value
unique_fields.append(field_name)
if null_found:
unique_queryset = self.__class__.objects.filter(**unique_filter)
if self.pk:
unique_queryset = unique_queryset.exclude(pk=self.pk)
if unique_queryset.exists():
msg = self.unique_error_message(self.__class__, tuple(unique_fields))
raise ValidationError(msg)
self.is_cleaned = True
def save(self, *args, **kwargs):
if not self.is_cleaned:
self.clean()
super().save(*args, **kwargs)
One possible workaround not mentioned yet is to create a dummy ModelA object to serve as your NULL value. Then you can rely on the database to enforce the uniqueness constraint.