Tastypie accessing fields from inherited models - django

Is it possible to include fields on related models, using tastypie?
As per my models below: if I persist one VideoContent and one TextContent instance to the DB, I can then get 2 objects back from my Content resource, however none of the additional fields are available.
Is it possible to include fields from related models (in this instance, the video url and the text content) and will that cater for adding more Content types in the future without having to rewrite the Content Resource, or am I coming at this from the wrong direction?
The goal is to be able to extend this with more ContentTypes without having to make changes to the Content resource (assuming it's possible to get it working in the first place)
Models.py:
class Content(models.Model):
parent = models.ForeignKey('Content', related_name='children', null=True, blank=True)
class TextContent(Content):
text = models.CharField(max_length=100)
class VideoContent(Content):
url = models.CharField(max_length=1000)
And then my resources:
class ContentResource(ModelResource):
children = fields.ToManyField('myapp.api.resources.ContentResource', 'children', null=True, full=True)
class Meta:
resource_name = 'content'
queryset = ContentResource.objects.all()
authorization = Authorization()
always_return_data = True

I found a good solution in another answer
Populating a tastypie resource for a multi-table inheritance Django model
I've run into the same problem - although I'm still in the middle of solving it. Two things that I've figured out so far:
django-model-utils provides an inheritence manager that lets you use the abstract base class to query it's table and can automatically downcast the query results.
One thing to look at is the dehydrate/rehydrate methods available to Resource classes.
This is what I did:
class CommandResource(ModelResource):
class Meta:
queryset = Command.objects.select_subclasses().all()
That only gets you half way - the resource must also include the dehydrate/rehydrate stuff because you have to manually package the object up for transmission (or recieving) from the user.
The thing I'm realizing now is that this is super hacky and there's gotta be a better/cleaner way provided by tastypie - they can't expect you to have to do this type of manual repackaging in these types of situations - but, maybe they do. I've only got about 8 hours of experience with tastypie # this point so if I'm explaining this all wrong perhaps some nice stackoverflow user can set me straight. :D :D :D

I had the same requirement and finally solved it.
I didn't like the answer given in the above link because I didn't like the idea of combining queryset and re-sorting.
Apparently, you can inherit multiple resources.
By subclassing multiple resources, you include the fields of the resources.
And since those fields are unique to each resource, I made them nullable in the init.
wonder if there's a way to list the parents only once. (There are two now. One for subclassing, and one in meta)
class SudaThreadResource(ThreadResource):
def __init__(self, *args, **kwargs):
super(SudaThreadResource, self).__init__(*args, **kwargs)
for field_name, field_object in self.fields.items():
# inherited_fields can be null
if field_name in self.Meta.inherited_fields:
field_object.null=True
class Meta(ThreadResource.Meta):
resource_name = 'thread_suda'
usedgoodthread_fields = UsedgoodThreadResource.Meta.fields[:]
userdiscountinfothread_fields = UserDiscountinfoThreadResource.Meta.fields[:]
staffdiscountinfothread_fields = StaffDiscountinfoThreadResource.Meta.fields[:]
bitem_checklistthread_fields = BitemChecklistThreadResource.Meta.fields[:]
parent_field_set = set(ThreadResource.Meta.fields[:])
field_set = set(
set(usedgoodthread_fields) |
set(userdiscountinfothread_fields) |
set(staffdiscountinfothread_fields) |
set(bitem_checklistthread_fields)
)
fields = list(field_set)
inherited_fields = list(field_set - parent_field_set)
queryset = forum_models.Thread.objects.not_deleted().exclude(
thread_type__in=(forum_const.THREAD_TYPE_MOMSDIARY, forum_const.THREAD_TYPE_SOCIAL_DISCOUNTINFO)
).select_subclasses()

Related

Make Django ORM automatically fetch properties

Say I have a Post model like this:
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
text_content = models.CharField()
#property
def comment_count(self):
return self.comments.count()
Say I need to get all the data for the post with ID 3. I know that I can retrieve the user along with the post in one query, by doing Post.objects.select_related('user').get(pk=3), allowing me to save a query. However, is there a way to automatically fetch comment_count as well?
I know I could use Post.objects.annotate(comment_count=Count('comments')) (which will require me to remove the property or change the name of the annotation, as there would be an AttributeError), however is it possible to make the ORM do that part automatically, since the property is declared in the model?
Although I could just add the .annotate, this can get very tedious when there are multiple properties and foreign keys that need to be fetched on multiple places.
Maybe this is not a perfect solution for you but you might find something similar that works. You could add a custom object manager, see docs
Then you could add a method like with_comment_count(). OR if you always want to annotate, then you can modify the initial queryset.
class PostManager(models.Manager):
def with_comment_count(self):
return self.annotate(
comment_count=Count('comments')
)
# Or this to always annotate
def get_queryset(self):
return super().get_queryset().annotate(
comment_count=Count('comments')
)
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
text_content = models.CharField()
objects = PostManager()
Then you could query like this.
post = Post.objects.get(pk=1).with_comment_count()
post.comment_count
Thanks to Felix Eklöf I managed to get it to work! As specified in the question, this was in the case in which there were multiple properties/foreign keys that need prefetching, and as such the ability to 'chain' prefetching was needed. This isn't possible by chaining PostManager methods, since they don't return a PostManager but a QuerySet, however I came up with a function that caches whatever it is asked to cache all at once, avoiding the need for chaining methods:
class PostManager(models.Manager):
def caching(self, *args):
query = self.all()
if 'views' in args:
query = query.annotate(view_count=Count('views'))
if 'comments' in args:
query = query.annotate(comment_count=Count('comments'))
if 'user' in args:
query = query.select_related('user')
if 'archives' in args:
query = query.prefetch_related('archives')
return query
class Post(models.Model):
objects = PostManager()
...
# caching can then be added by doing, this, for instance:
Post.objects.caching('views', 'user', 'archives').get(id=3)
Note that for this to work, I also had to change #property to #cached_property, thus allowing the ORM to replace the value, and allow proper caching with the same name.

Django - edit both sides of a many-to-many relation with generic UpdateView

I have a question whether or not it is possible to use the generic UpdateView class to edit "both sides" of a many-to-many relationship.
I have the following classes defined in models.py:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
allowed_categories = models.ManyToManyField(SomeCategory)
These are both dictionary type tables that store sets of configuration data for my application. To allow editing the dictionaries I use simple UpdateViews:
class SomeClassUpdate(UpdateView):
model = SomeClass
template_name = 'admin/edit_class.html'
fields = ['code', 'name', 'age', 'allowed_categories']
ordering = ['code']
This works fine, I get a nice multi-select and everything is perfect. However, I would like to have the possibility to edit the relationship from the side of the SomeCategory table, so I can choose which SomeClass elements are linked to a certain SomeCategory:
class SomeCategoryUpdate(UpdateView):
model = SomeCategory
template_name = 'admin/edit_category.html'
fields = ['code', 'name', ??????? ]
ordering = ['code']
I have tried adding the related_name attribute to the SomeCategory model, but that did not work.
Any ideas if this can be done without using a custom ModelForm?
Key library versions:
Django==1.11.8
psycopg2==2.7.4
PS: this is my very first question asked on stackoverflow, so please let me know if my post is missing any mandatory elements.
Your issue is in the models.py file. You have two classes, but only one of them mentions the other one. You would think that this should be enough since you are using ManyToManyField after all and assume that it would automatically create every connection leading both ways... Unfortunately this is not true. On the database level it does indeed create a separate intermediary table with references to objects in both original tables, but that doesn't mean that both of them will be automatically visible in Django Admin or similar.
If you would attempt to simply create another someclass = models.ManyToManyField(SomeClass) in the SomeCategory class that would fail. Django would try to create another separate intermediary table through which the connection between two main tables is established. But because the name of the intermediary table depends on where you define the ManyToManyField connection, the second table would be created with a different name and everything would just logically collapse (two tables having two separate default ways to have a ManyToMany connection makes no sense).
The solution is to add a ManyToManyField connection to SomeCategory while also referencing that intermediary/through table that was originally created in the SomeClass class.
A couple of notes about Django/python/naming/programming conventions:
Use the name of the table you are referencing to, as the name of the field that is containing the info about that connection. Meaning that SomeClass's field with a link to SomeCategory should be named somecategory instead of allowed_categories.
If the connection is one-to-many - use singular form; if the connection is many-to-many - use plural. Meaning that in this case we should use plural and use somecategories instead of somecategory.
Django can automatically pluralize names, but it does it badly - it simply adds s letter to the end. Mouse -> Mouses, Category -> Categorys. In those kind of cases you have to help it by defining the verbose_name_plural in the special Meta class.
Using references to other classes without extra 's works only if the the class was already defined previously in the code. In the case of two classes referring to each other that is true only one way. The solution is to put the name of the referred class in the quotation marks like 'SomeCategory' instead of SomeCategory. This sort of reference, called a lazy relationship, can be useful when resolving circular import dependencies between two applications. And since by default it's better to keep the style the same and to avoid unnecessary brain energy wasting of "I will decide whether or not to use quotation marks depending on the order the classes have been organized; I will have to redo this quotation marks thingie every time I decide to move some code pieces around" I recommend that you simply use quotation marks every time. Just like when learning to drive a car - it's better to learn to always use turn signals instead of first looking around and making a separate decision of whether someone would benefit from that information.
"Stringifying" (lazy loading) model/class/table name is easy - just add 's around. You would think that stringifying the "through" table reference would work the same easy way. And you would be wrong - it will give you the ValueError: Invalid model reference. String model references must be of the form 'app_label.ModelName'. error. In order to reference the stringified "through" table you need to: (a) add 's around; (b) replace all dots (.) with underscores (_); (c) delete the reference to through!.. So SomeClass.somecategories.through becomes 'SomeClass_somecategories'.
Therefore the solution is this:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
someclasses = models.ManyToManyField('SomeClass', through='SomeClass_somecategories', blank=True)
class Meta:
verbose_name_plural = 'SomeCategories'
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
somecategories = models.ManyToManyField('SomeCategory')
After this it should be obvious what kind of final changes to make to your UpdateView classes.
You can achieve this in the view and form, without having to specify the additional ManytoMany connections in the
models, using something like the following:
In the View
class SomeClassUpdate(UpdateView):
model = SomeClass
form_class = SomeClassUpdateForm # to specify the form
template_name = 'admin/edit_class.html'
def form_valid(self, form, *args, **kwargs):
initial_somecategorys = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
amended_somecategorys = form.cleaned_data['allowed_categroies']
remove = [x for x in initial_somecategorys if x not in amended_somecategorys]
add = [x for x in amended_somecategorys if x not in initial_somecategorys]
for somecategory in add:
somecategory.allowed_categories.add(form.instance)
somecategory.save()
for somecategory in remove:
somecategory.allowed_categories.remove(form.instance)
somecategory.save()
return super().form_valid(form)
In the Form
The init method at the top pre-populates the form with entries saved on the model.
class SomeClassUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SomeClassUpdateForm, self).__init__(*args, **kwargs)
try:
obj = kwargs['instance']
self.fields["some_categories"].initial = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
except (AttributeError, KeyError): # to catch NoneType if new entry being created.
pass
some_categories = forms.ModelMultipleChoiceField(
required=False,
queryset=SomeCategory.objects.all(),
)
class Meta:
model = SomeClass
fields = [
'some_categories'
..etc
]
This should work. I've writen similar code in one of my projects, and it's working fine. However, I don't know if it's
structurally best to use methods like this and not alter the model relationships or whether it's preferable to
alter the model relationships as outlined in other replies. So I'd be interested to know other peoples views on what
the best approach is.

Custom django foreignfield

Anybody knows how to create a foreignkey field and make it always point to same model, so far I got these.
class PanMachineTimeUnitField(models.ForeignKey):
def __init__(self, **kwargs):
to = 'panbas.PanBasTimeUnit'
kwargs['verbose_name'] = _('Machine Unit')
kwargs['related_name'] = 'machine_unit'
super(PanMachineTimeUnitField, self).__init__(to, **kwargs)
But I got errors when on start.
I aim to use it like,
machine_unit = PanMachineTimeUnitField()
No further declarations needed.
Edit:
I want this because, I will have this foreignkey in quiet a few places. If I want to change the verbose_name of field, I want all of my fields to be affected by this change. Verbose name was an example, it may be an another attribute.
I dont want to use settings py to declare the defaults, either.
I recommend that you use only a simple function to create a similarly pre-configured instance of ForeignKey: (not an instance of subclass of ForeignKey)
def pan_machine_time_unit_field(**kwargs):
othermodel = 'panbas.PanBasTimeUnit'
on_delete = models.DO_NOTHING # or what you need
kwargs['verbose_name'] = 'Machine Unit'
kwargs.setdefault('related_name', '+')
# or: kwargs.setdefault('related_name', "%(app_label)s_%(class)s_related",
return models.ForeignKey(othermodel, on_delete, **kwargs)
class C(models.Model):
machine_unit = pan_machine_time_unit_field()
# or:
# machine_unit = pan_machine_time_unit_field(related_name='klass_c_children')
The related_name attribute is a name used for backward relation from the target object of othermodel to all objects that reference it. That name must be unique on othermodel ('panbas.PanBasTimeUnit', usually something with app and class name that is unique enough) or that name can be '+' if you don't want to create a backward relationship query set. Both variants are implied in the example. Also remember on_delete.
If you would really need to create a subclass (which makes sense if more methods need be customized), you must also define a deconstruct method for migrations. It would be complicated if you need to modify such subclass later. It can be never removed, renamed etc. due to migrations on a custom field. On the other hand, if you create a simple instance of ForeignKey directly by a function, all about migrations can be ignored.
EDIT
Alternatively you can create an abstract base model with that field and create new models by inheritance or multiple inheritance:
class WithPanBasTimeUnit(models.Model):
machine_unit = models.ForeignKey(
'panbas.PanBasTimeUnit',
models.DO_NOTHING,
verbose_name=_('Machine Unit'),
related_name='%(app_label)s_%(class)s_related'
)
class Meta:
abstract = True
class ExampleModel(WithPanBasTimeUnit, ...or more possible base models...):
... other fields
This solution (inspired by an invalid soution Ykh) useful if you want to add a method to models with that field or to add more fields together, otherwise the original solution is easier.
class PanBasTimeUnit(models.Model):
machine_unit = models.ForeignKey('self', blank=True, null=True,
verbose_name=u'parent')
use 'self' or 'panbas.PanBasTimeUnit' will fine.
You can not have several Foreign Keys to a model with same related_name.
Indeed, on a PanBasTimeUnit instance, which manager should Django return when calling <instance>.machine_unit? This is why you have to be carefull on related models and abstract classes.
It should work fine if you remove kwargs['related_name'] = 'machine_unit' in your code, and replace it with kwargs['related_name'] = "%(app_label)s_%(class)s_related" or something similar.
A slight modification in your attempt should do your work.
class PanMachineTimeUnitField(models.ForeignKey):
def __init__(self, **kwargs):
kwargs["to"] = 'panbas.PanBasTimeUnit'
kwargs['verbose_name'] = _('Machine Unit')
kwargs['related_name'] = 'machine_unit'
super(PanMachineTimeUnitField, self).__init__(**kwargs)
why not use directly machine_unit = models.ForeignKey(panbas.PanBasTimeUnit, verbose_name=_('Machine Unit'), related_name='machine_unit')) ?

Trouble overriding save method on Django model with ManyToManyField

I'm having trouble overriding the save method on a Django model to check a restriction on a many-to-many field.
Say I have the following models:
class Person(models.Model):
name = models.CharField()
class ClothingItem(models.Model):
description = models.CharField()
owner = models.ForeignKey(Person)
class Outfit(models.Model):
name = models.CharField()
owner = models.ForeignKey(Person)
clothing_items = models.ManyToManyField(ClothingItem)
I would like to put a restriction on the save method of Outfit that ensures that each ClothingItem in a given outfit has the same owner as the Outfit itself.
I.e. I'd like to write:
class Outfit(models.Model):
name = models.CharField()
owner = models.ForeignKey(Person)
clothing_items = models.ManyToManyField(ClothingItem)
def save(self, *args, **kwargs):
for ci in self.clothing_items:
if ci.owner != self.owner:
raise ValueError('You can only put your own items in an outfit!)
super(Outfit, self).save(*args, **kwargs)
but when I try that I get an error about <Outfit: SundayBest>" needs to have a value for field "outfit" before this many-to-many relationship can be used.
Any ideas what's going wrong here?
There are two issues going on here. To directly answer your question, the error basically means: You cannot refer to any m2m relationship if the original object(an instance of Outfit here) is not saved in database.
Sounds like you are trying to do the validation in save() method, which is a pretty bad practice in django. The verification process should typically happen in Form that creates Outfit objects. To override default django form, please refer to django ModelAdmin.form. To understand how to do validation on django forms, check ModelForm validation.
If you want code to refer to for m2m validation, I found a good example from SO.

How can I limit fields in ToManyField with full=True in django-tastypie

I have the following resource:
class MachineResource(ModelResource):
manager = fields.ToOneField(UserResource, 'manager',full=True)
class Meta:
queryset = Service.objects.filter(service_type='machine')
resource_name = 'machine'
This works fine. And will return a list of machines, and an embedded user object (the manager) in each.
However, I only one want 2-3 fields from the manager user. I dont want it to contain the managers salted pass and other private data for example.
As far as I can see there isn't a way I can do this easily?
Just take a look at the Quick Start section for django-tastypie. There's a perfect example right there. When you define your ModelResource subclass for User (your "manager"), simply add a Meta class with an exclude attribute set to the list of fields you don't want to show.
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
I personally find the notion of creating two ModelResources for the same Model class a bit inelegant. For instance, suppose you wanted to display the email field in the detail view of UserResource but not while being displayed as a full object as part of the MachineResource. The way I would solve your problem is by deleting the non-required field's key in the data dictionary of the embedded object in the dehydrate method. A bit hacky way maybe, but works fine for me. For your case, you can do:
class MachineResource(ModelResource):
manager = fields.ToOneField(UserResource, 'manager',full=True)
class Meta:
queryset = Service.objects.filter(service_type='machine')
resource_name = 'machine'
def dehydrate(self,bundle):
del bundle.data['manager'].data['email']
return bundle