Django m2m 'through' with a generic foreignkey - django-models

I have the follow code example, which is a simplified abstraction of a real world project I'm working on:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
class FeatureSet(models.Model):
"""
Feature Set
"""
name = models.CharField(max_length=255)
def __unicode__(self):
return u"%s" % self.name
class GenericObjectAlpha(models.Model):
title = models.CharField(max_length=255)
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
def __unicode__(self):
return u"%s" % self.title
class GenericObjectBeta(models.Model):
title = models.CharField(max_length=255)
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
def __unicode__(self):
return u"%s" % self.title
class Feature(models.Model):
"""
Feature
"""
# FK to feature set
feature_set = models.ForeignKey(FeatureSet)
# FK to generic object, Generic object alpha or beta... or others
content_type = models.ForeignKey(
ContentType,
default='article',
limit_choices_to={ 'model__in': ('genericobjectalpha', 'genericobjectbeta') },
related_name="play__feature_set__feature")
object_id = models.PositiveIntegerField(
"Feature object lookup")
content_object = generic.GenericForeignKey(
'content_type',
'object_id')
# Extra fields on a m2m relationship
active = models.BooleanField()
order = models.PositiveIntegerField()
def __unicode__(self):
return u"%s::%s" % (self.feature_set, self.content_object)
This line causes an error:
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
Obviously because the 'through' model lacks a corresponding FK to each side of the m2m. What I'd like to achieve here, is that one side of the m2m relationship is generic, and, that I can specify my own intermediary join table, to do the usual adding of custom fields etc.
What are my options for accomplishing this?
Note, its currently an important requirement to include the feature_sets = models.ManyToManyField(FeatureSet, through='Feature') line in the generic model, mostly for admin UI purposes. The reason why its generic is that its not yet determined how many models this line will be placed upon.

Related

Adding data to multiple tables using django rest serializers

I am working on django rest framework coupled with react frontend. I am building a simple web service for a medium-sized organisation.
Here is my Invoice model from models.py
class Invoice(models.Model):
invoice_id = models.AutoField(primary_key=True, editable=False)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
date = models.DateField(auto_now_add=True)
builty_num = models.CharField(max_length=100)
total_amount = models.FloatField(validators=[MinValueValidator(0.0)])
class Meta:
ordering = ['date', 'customer']
InvoiceDetail from models.py
class InvoiceDetail(models.Model):
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
warehouse = models.ForeignKey(Warehouse, on_delete=models.SET_NULL, null=True)
rate = models.FloatField(validators=[MinValueValidator(0.0)])
num_thaan = models.PositiveIntegerField()
gazaana_per_thaan = models.FloatField(validators=[MinValueValidator(0.0)])
Ledger model
class Ledger(models.Model):
TYPE = (
('D','Debit'),
('C','Credit'),
)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
date = models.DateField(auto_now_add=True)
detail = models.TextField()
transaction_type = models.CharField(max_length=1, choices=TYPE)
amount = models.FloatField(validators=[MinValueValidator(0.0)])
Now I will be having a form on my front-end which will be sending customer_id and an array of InvoiceDetail which I want to fill my database with. So, I will be using information from that form and use it to fill up multiple entries in my InvoiceDetail and the total_amount would also be calculated from the invoice detail. After this, I would also fill up the ledger based on
the total_amount. Right now, I am unable to figure out a way to do this as I am just able to manipulate a single table via a viewset
P.S. I am using these serializers right now with DefaultRouter()
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
class WarehouseSerializer(serializers.ModelSerializer):
class Meta:
model = Warehouse
fields = '__all__'
class ExpenseSerializer(serializers.ModelSerializer):
class Meta:
model = Expense
fields = '__all__'
class InvoiceSerialiser(serializers.ModelSerializer):
class Meta:
model = Invoice
fields = '__all__'
And this is my views.py
class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
class WarehouseViewSet(viewsets.ModelViewSet):
serializer_class = WarehouseSerializer
queryset = Warehouse.objects.all()
class ExpenseViewSet(viewsets.ModelViewSet):
serializer_class = ExpenseSerializer
queryset = Expense.objects.all()
In Django REST Framework (DRF), by using Nested Relationships of Serializer Relations data can be added to multiple tables having relationships.
In Nested Relationships, the referred entity can be embedded or nested in the representation of the object that refers to it. Such nested relationships can be expressed by using serializers as fields. If we want to support write-operations to a nested serializer field we need to have create() and/or update() methods in order to explicitly specify how the child relationships should be saved. It may be noted that by default nested serializers are read-only.
Below is the implementation of the relationship between Invoice and InvoiceDetail.
class InvoiceDetailSerialiser(serializers.ModelSerializer):
class Meta:
model = InvoiceDetail
fields = "__all__"
read_only_fields = ("invoice", )
It is required to make invoice readonly in InvoiceDetailSerialiser.
class InvoiceSerialiser(serializers.ModelSerializer):
invoice_details = InvoiceDetailSerialiser(many=True)
class Meta:
model = Invoice
fields = ('invoice_id', 'customer', 'date', 'builty_num', 'total_amount', 'invoice_details')
def create(self, validated_data):
invoice_details_data = validated_data.pop('invoice_details')
invoice= Invoice.objects.create(**validated_data)
for invoice_detail_data in invoice_details_data :
InvoiceDetail.objects.create(invoice=invoice, **invoice_detail_data )
return invoice
If the field is used to represent a to-many relationship, we should add the many=True flag to the serializer field.
Based on the above explanation, rest of the implementations can be developed.
More details on this topic from the official guide can be found here.

Serializer field for side effect model django rest framework

I have a django.db.models.Model A whose instances are created in a rest_framework.serializers.ModelSerializer from POST requests.
Depending on the data being sent in the POST, I would like to create one of several other "addon" models, let's say B or C, which I link to the original through a django.db.models.OneToOneField:
from django.db import models
class A(models.Model):
some_field = models.CharField()
class B(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
class C(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
What I would like to is to have a serializer which validates the incoming data, including some string indicating which addon to use. The serializer then creates the model instance of A and based on this creates the addon model.
I do not want to create a utility field in model A used to determine which addon to use, I would like to create the model directly using the instance of model A and information from the POST itself.
At the same time when accessing the data through a get, I would like to return the original string used to determine which addon to use.
What I have come up with so far:
from rest_framework import serializers
str2model = {'b': B, 'c': C}
class AddonField(serializers.Field):
def to_representation(self, value):
# I completely ignore "value" as no "internal value" is set in "to_internal_value"
myvalue = self.parent.instance.addon
for addon_name, addon_class in str2model.items():
if isinstance(myvalue, addon_class):
return addon_name
def to_internal_value(self, data):
# I create the "internal value" after "A" instance is created, thus here I do nothing?
return data
class ASerializer(serializers.ModelSerializer):
some_field = serializers.CharField()
the_addon = AddonField()
def validate_the_addon(self, value): # here addon is a string
if value in str2model.keys():
return value
def create(self, validated_data):
addon_name = validated_data.pop('the_addon')
addon_class = str2model[addon]
a = super(ASerializer, self).create(validated_data)
addon_class.objects.create(a=a)
return a
class Meta:
model = A
fields = ["some_field", "the_addon"]
When testing this I get:
AttributeError: Got AttributeError when attempting to get a value for field `the_addon` on serializer `ASerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `A` instance.
Original exception text was: 'A' object has no attribute 'the_addon'.
How can I temporarily store the_addon in the serializer until the A instance has been created?
This is how I would typically approach it
# Serializer
class ASerializer(serializers.Serializer):
some_field = serializers.CharField()
addon_b = serializers.CharField(required=False, allow_null=True)
addon_c = serializers.CharField(required=False, allow_null=True)
def create(self, validated_data):
addon_b = validated_data.pop('addon_b')
addon_c = validated_data.pop('addon_c')
a = A.objects.create(some_field=validated_data['some_field'])
if addon_b:
B.objects.create(a=a)
if addon_c:
C.objects.create(a=a)
return a
You can do other validations if necessary.
class TestAPIView01(generics.CreateAPIView):
permission_classes = {}
serializer_class = serializers.ASerializer
queryset = A.objects.all()
Also, look at the related_name on B and C you may want to consider making them different, as that might throw an error in the future. Cheers

custom intermediate table django fetch the records

I have two models with M2M relation. The custom table is defined as with en extra field
class DoctorHospital(models.Model):
clinic = models.ForeignKey(ClinicHospital, on_delete=models.CASCADE)
doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
shift = models.CharField(max_length = 10)
Problem is that I am trying to fetch all clinics based on a specific doctor. Data is come based on specific doctor but custom field shift does not come.
here is my class base view
class DoctorDetailView(generic.DetailView):
model = Doctor
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['clinic_hospital_list'] = self.object.clinic_hospital.all()
return context
You can annotate the clinic_hospital_list to obtain the related shift value:
from django.db.models import F
class DoctorDetailView(generic.DetailView):
model = Doctor
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['clinic_hospital_list'] = self.object.clinic_hospital.annotate(
shift=F('doctorhospital__shift')
)
return context
Now the ClinicHospitals that arise from this queryset will have an extra attribute .shift that holds the value of the shift field in the intermediate table.

Django DRF Foreign Key

This question or many like it has been asked multiple times but for some reason I am unable to find the answer.
I do have this working to an extent in the way that if you go on the api pages, it renders, creates and updates without problem. The issue is displaying a field (title) from the nested object instead of just the primary key on the front end.
Some background before getting into the code:
Races is a finite list (e.g. Race1, Race2, Race3) and the front end does not have the ability to add more.
Cards is not finite, but each card must link to an existing Race (this currently does so by Primary Key).
The front end should display the card_text and race title of the linked race.
It also has the ability to add a new card but this works fine.
I have had this working with separate serializers for read and create/update where the read has a 'depth = 1' to pull through the entire object but the create/update doesn't and you then parse the object and send the primary key back (I couldn't find a way of doing this in the serializer, is it possible?).
So basically my question is, are you meant to pass the entire object through and parse it on a POST method, or do you pass the primary key and pull in the linked objects (Races) and use the primary key as an index (e.g. Races[card_race]). Also, why is 'linked_race' not coming through to the front end?
I realise I've almost answered my own question but as I'm new to Django I'm looking for the correct conventions and who knows, it may save someone else time when searching for the same answer.
urls.py
from .api import CardViewSet, RaceViewSet
from rest_framework.routers import DefaultRouter
from django.conf.urls import url, include
from .views import landing
router = DefaultRouter()
router.register(r'cards', CardViewSet)
router.register(r'races', RaceViewSet)
urlpatterns = [
url(r'^$', landing),
url(r'^api/', include(router.urls)),
]
api.py
from rest_framework.viewsets import ModelViewSet
from .serializers import CardSerializer, RaceSerializer
from .models import Card, Race
class CardViewSet(ModelViewSet):
queryset = Card.objects.filter(active=True)
def get_serializer_class(self):
return CardSerializer
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
class RaceViewSet(ModelViewSet):
queryset = Race.objects.filter(active=True)
serializer_class = RaceSerializer
models.py
from django.db import models
from django.conf import settings
User = settings.AUTH_USER_MODEL
class Race(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=30, blank=False)
active = models.BooleanField(default=True)
def __str__(self):
return "{}".format(self.title)
def __unicode__(self):
return self.title
class Card(models.Model):
card_text = models.CharField(max_length=100, blank=False)
card_description = models.CharField(max_length=100, blank=True)
card_race = models.ForeignKey(Race, related_name='linked_race', on_delete=models.CASCADE)
creator = models.ForeignKey('auth.User', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.card_text
class Meta:
ordering = ('created',)
serializers.py
from rest_framework import serializers
from .models import Card, Race
class RaceSerializer(serializers.ModelSerializer):
class Meta:
model = Race
fields = '__all__'
class CardSerializer(serializers.ModelSerializer):
linked_race = RaceSerializer(read_only=True, many=True)
class Meta:
model = Card
fields = 'id', 'card_text', 'card_description', 'card_race', 'linked_race',
Javascript extract (AngularJS)
$http.get('/api/races/').then(function (response) {
$scope.races = response.data;
$scope.selectedOption = $scope.races[0];
});
$scope.cards = [];
$http.get('/api/cards/').then(function (response) {
$scope.cards = orderBy(response.data, 'created', true);
});
html extract (AngularJS)
<div class="races--row" ng-repeat="c in cards | filter : card_filter |
orderBy : sortVal : sortDir" ng-class-odd="'odd'" ng-click="openModal(c)">
<div class="races--cell race">{{ c.card_race.title }}</div>
<div class="races--cell card-text">{{ c.card_text }}</div>
</div>
Your first "problem" is with the Card model (I say problem because I don't think you intended to do this). You're defining related_name='linked_race' for the card_race field. This related_name is the name you use to refer to a card FROM a race.
I would suggest you leave it out and use the default that Django already gives us (i.e. my_race.card_set.all() in this case). So change change that field in the Card model to:
class Card(models.Model):
...
card_race = models.ForeignKey(Race, on_delete=models.CASCADE)
...
And let's change the card serializer to:
class CardSerializer(serializers.ModelSerializer):
# no more linked_race
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race')
Alright, this is a vary basic model serializer and you won't see details of a race yet. So now let's get to your main problem which was that you wanted to:
see the details of the associated race of a card
perform create/get/update/delete operations using the same serializer
For this, let's further change the CardSerializer to include another field called race_detail:
class CardSerializer(serializers.ModelSerializer):
race_detail = RaceSerializer(source='card_race', read_only=True)
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race', 'race_detail')
We have defined two serializer fields for the same model field. Note the source and read_only attributes. This makes this field available when you GET a card (which is what we want), but not when you're performing POSTs or PUTs (which avoids the problem of sending the whole race object and parsing and stuff). You can just send the race id for the card_race field and it should work.

URL with dot in Django Rest Framework

I have the following model with a primary_key=True specified:
class Team(models.Model):
name = models.CharField(
max_length=64,
primary_key=True,
)
... other fields
When I serialize this model, I do the following:
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ('url', 'name',) # and another fields
My viewset:
class TeamViewSet(viewsets.ModelViewSet):
lookup_value_regex = '[-\w.]'
queryset = Team.objects.all()
serializer_class = TeamSerializer
filter_fields = ('name',) # and another fields
My urls.py:
router = routers.DefaultRouter()
router.register(r'teams', TeamViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),
# I am not sure if this url is right. I repeat of include(router.urls)
url(r'^api/teams/(?P<name>[-\w.]+)/', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
Then, when I create a Team object with name attribute containing dot ., for example Latinos F.C. and I go to the rest url, I get:
I am not sure about of how to use the lookup_value_regex attribute in my viewset. In this answer is used with some basic regex, but if I use it, any Team object is reachable via my serialized Rest API.
How to can I get a url like as: /api/teams/Name.F.C. in my serialized Team model?
First of all check if you have set APPEND_SLASH to True in your settings, because if not - the missing slash (at the end of the URL) is a problem.
Second - I do not think that dot is a problem, the problem can be a space - coded as %20;
Third - such urls looks just ugly :) You should consider changing it to some kind of a slugs: Latinos F.C. -> latinos-fc;
If you do that (just add additional field on the model with slug - this field should be obviously unique) - set up the lookup_field on your view - this will solve your problem.
Consider the example:
views.py
class SomeViewSet(viewsets.ModelViewSet):
queryset = SomeModel.objects.all()
serializer_class = SomeSerializer
lookup_field = 'slug_name'
serializers.py
class SomeSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('id', 'name', 'slug_name')
read_only_fields = ('slug_name',)
def to_internal_value(self, data):
ret = super(SomeSerializer, self).to_internal_value(data)
ret['slug_name'] = slugify(ret['name'])
return ret
models.py
class SomeModel(models.Model):
name = models.CharField(max_length=100)
slug_name = models.SlugField(unique=True, max_length=100)
urls.py
router.register(r'teams', SomeViewSet, base_name='teams')
urlpatterns = router.urls
And now:
creation:
details:
Can you do that this way? Or you really need the dots?

Resources