Two step object creation in Django Admin - django

I'm trying to change the implementation of an EAV model using a JSONField to store all the attributes defined by an attribute_set.
I already figured out how to build a form to edit the single attributes of the JSON, but I'm currently stuck at implementing the creation of a new object. I think I have to split object creation in two steps, because I need to know the attribute_set to generate the correct form, but I don't know if there's a way to hook in the create action, or any other way to achieve what I need.
My models look like this:
class EavAttribute(models.Model):
entity_type = models.CharField(max_length=25, choices=entity_types)
code = models.CharField(max_length=30)
name = models.CharField(max_length=50)
data_type = models.CharField(max_length=30, choices=data_types)
class AttributeSet(models.Model):
name = models.CharField(max_length=25)
attributes = models.ManyToManyField('EavAttribute')
class EntityAbstract(models.Model):
attribute_set = models.ForeignKey(
'AttributeSet',
blank=False,
null=False,
unique=False,
)
class Meta:
abstract = True
class Event(EntityAbstract):
entity_type = models.CharField(max_length=20, null=False, choices=entity_types, default=DEFAULT_ENTITY_TYPE)
code = models.CharField(max_length=25, null=True, blank=True, db_index=True)
year = models.IntegerField(db_index=True)
begin_date = models.DateField()
end_date = models.DateField()
data = JSONField()
How can I choose the AttributeSet first and then go to another form that I would populate with the attributes in the chosen attribute set?

I ended up using get_fields() and response_add() methods, like so:
def get_fields(self, request, obj=None):
if obj is None:
return ['attribute_set']
else:
return [attr.name for attr in obj._meta.get_fields() if not attr.auto_created and attr.name != 'id']
def get_readonly_fields(self, request, obj=None):
readonly_fields = ['entity_type', 'code', 'state']
if obj is not None:
readonly_fields.append('attribute_set')
return readonly_fields
def response_add(self, request, obj, post_url_continue=None):
url = '/admin/risks/event/{}/change/'.format(obj.id)
return redirect(url)
The downside of this approach is that object is saved in the database and then opened for edit, so basically the database is hit twice and all attributes have to be nullable, except for attribute_set.
I would be happy to receive ideas for better implementations.

Related

unique_together does not prevent duplicate records

I have two models like this :
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
def __str__(self):
return self.user.username
class Meta:
unique_together = ('user', 'place_detail',)
this is the json i post to my apiview :
{
"lat": "29.621142463088336",
"lng": "52.520185499694527",
"name":"cafesama1",
"address":"streetn1",
"type":"cafe"
}
in views.py :
class PreferedLocationsOfUsers(APIView):
def post(self, request, format=None):
serializer = PreferLocationSerializer(data=request.data)
if serializer.is_valid():
location= Preference(**serializer.data)
location.save()
user_perefeces = PrefenceOfUser(user=request.user,place_detail=location)
user_perefeces.save()
return Response({'location saved'},status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
i want to prevent the user to save dublicated records in database but when location object is saved in PrefenceOfUser unique_together does not prevent dublicating. any idea why?
You do have a migration issue as per the comments. Get rid of the duplicates, re-run the migration until you get no errors and you're fine.
However, I would turn this around as a model design problem. You shouldn't need to resort to manual constraints for this.
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
users = models.ManyToManyField(get_user_model(), blank=True)
This above code will have exactly the same implications as yours, is cleaner and more Djangoesque.
You can still access what you call PrefenceOfUser through Preference.users.through. If you plan to add more attributes to the selection (ie. when did user add their preference), you can just do:
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
extra_field = models.WhateverField()
def __str__(self):
return self.user.username
and change the Preference.users to
models.ManyToManyField(get_user_model(), through=PrefenceOfUser)
which will still still ensure the uniqueness.

How to add an extra parameter to a Django object when creating it?

I have a Django model that has many parameters, amongst which is one called 'system' which is a reference to an object of type 'System'.
class Format(models.Model):
system = models.ForeignKey(System, on_delete=models.SET_NULL, null=True)
petition_date = models.DateField(auto_now=True)
resolution_date = models.DateField(null=True, default=None)
... other fields ...
The system may be null, but I'd want this field to be the same as the user that creates the Format object.
My user object is like this:
class CustomUser(AbstractUser):
current_system = models.ForeignKey(System, on_delete=models.SET_NULL, null=True, default=None)
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, default=None)
I have a form which successfully creates the Format objects. However, I'd like to know a way to do something like this in my CreateView:
class FormatCreateView(LoginRequiredMixin, CreateView):
"""
-----------------------Vista de crear nuevos articulos--------------------------
"""
template_name = 'format_form.html'
model = Format
success_url = reverse_lazy("formats:formats")
form_class = FormatUpdateForm
def post(self, request, *args, **kwargs):
form = FormatUpdateForm(request.POST)
if form.is_valid():
user = User.objects.get(id=self.request.user.id)
format = self.object # I'm pretty sure this is wrong and doesn't get the Format object that was just created
format.system = user.current_system
format.save(update_fields=['system'])
return HttpResponseRedirect(reverse('formats:formats'))
This, however, doesn't work. Any suggestions would be appreciated.
UPDATE:
Also, I forgot to mention, when doing that, the Format object is no longer being created after completing and sending the form.

Django ModelChoiceField Issue

I've got the following Situation, I have a rather large legacy model (which works nonetheless well) and need one of its fields as a distinct dropdown for one of my forms:
Legacy Table:
class SummaryView(models.Model):
...
Period = models.CharField(db_column='Period', max_length=10, blank=True, null=True)
...
def __str__(self):
return self.Period
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'MC_AUT_SummaryView'
Internal Model:
class BillCycle(models.Model):
...
Name = models.CharField(max_length=100, verbose_name='Name')
Period = models.CharField(max_length=10, null=True, blank=True)
Version = models.FloatField(verbose_name='Version', default=1.0)
Type = models.CharField(max_length=100, verbose_name='Type', choices=billcycle_type_choices)
Association = models.ForeignKey(BillCycleAssociation, on_delete=models.DO_NOTHING)
...
def __str__(self):
return self.Name
Since I don't want to connect them via a Foreign Key (as the SummaryView is not managed by Django) I tried a solution which I already used quite a few times. In my forms I create a ModelChoiceField which points to my Legacy Model:
class BillcycleModelForm(forms.ModelForm):
period_tmp = forms.ModelChoiceField(queryset=SummaryView.objects.values_list('Period', flat=True).distinct(),
required=False, label='Period')
....
class Meta:
model = BillCycle
fields = ['Name', 'Type', 'Association', 'period_tmp']
And in my view I try to over-write the Period Field from my internal Model with users form input:
def billcycle_create(request, template_name='XXX'):
form = BillcycleModelForm(request.POST or None)
data = request.POST.copy()
username = request.user
print("Data:")
print(data)
if form.is_valid():
initial_obj = form.save(commit=False)
initial_obj.ModifiedBy = username
initial_obj.Period = form.cleaned_data['period_tmp']
initial_obj.Status = 'Creating...'
print("initial object:")
print(initial_obj)
form.save()
....
So far so good:
Drop Down is rendered correctly
In my print Statement in the View ("data") I see that the desired infos are there:
'Type': ['Create/Delta'], 'Association': ['CP'], 'period_tmp': ['2019-12']
Still I get a Select a valid choice. That choice is not one of the available choices. Error in the forms. Any ideas??

How to set automatically 'many' flag of django serializer depending on input being a list or a single item

I have the following machine model.
class Machine(models.Model):
operators = models.ManyToManyField(User, related_name='machines', blank=True)
elasticsearch_id = models.PositiveIntegerField(default=None, null=True, blank=True)
company = models.ForeignKey(Company, default=None, null=True, blank=True,on_delete=models.SET_DEFAULT)
machine_brand = models.CharField(max_length=200, null=False)
machine_model = models.CharField(max_length=200, default='')
machine_picture = models.URLField(max_length=200, null=True)
tools = models.ManyToManyField('Tool', default=None, blank=True)
clustered_tags = JSONField(null=True)
elasticsearch_tags = JSONField(null=True, blank=True, default=DEFAULT_TAG_MAP)
machine_slug = models.SlugField()
With the following serializer.
class MachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
fields = '__all__'
In my views, I am filtering the data on the company the logged in users belongs to. Now, I want to serialize the object and return it to the client. However, I don't know beforehand whether the queryset is a list of objects or a single object so that I can set the many flag of the serializer to true or false.
#api_view(['GET','POST'])
def manage_operators(request):
user_machines = Machine.objects.filter(company=request.user.company)
user_machines_ser = MachineSerializer(user_machines, many=True)
return Response({'machines': user_machines_ser.data})
Is there any elegant way to solve this? I could solve it this way but there must be a better way of doing it.
if len(user_machines) > 0 :
user_machine_ser = MachineSerializer(user_machines, many=True)
else:
user_machine_ser = MachineSerializer(user_machines, many=False)
Any input much appreciated!
Since you are fetching a QuerySet every time, you don't have to set many=False if there is only one item in the QuerySet.
So you can safely use
user_machine_ser = MachineSerializer(user_machines, many=True)
everytime, no matter how many objects are in the QuerySet.
Since you are passing a QuerySet, you can use the count() [Django doc] method in the __init__() method of MachineSerializer by overriding it.
class MachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if kwargs['instance'].count() > 1: # count() method used here <<<<<
kwargs['many'] = True
else:
kwargs['many'] = False

Django Rest Framework - get foreignkey id from value when inserting

I can not get a clear answer after two days of searching for what must probably be one of the most common things to do with a DRF:
I have the following model:
class ProcessedStockAmounts(models.Model):
prodName = models.ForeignKey(Productlist, on_delete=models.CASCADE, blank=False, unique=False)
amount = models.CharField(unique=False, max_length=255)
time = models.ForeignKey(StockTakingTimes, on_delete=models.CASCADE, blank=False, unique=False, default=1)
def __str__(self):
return str(self.prodName)
And I am returning a JSON object via my API that looks like this:
[{'prodName': 'SV1', 'amount': '1111111', 'time' : 1}]
When I insert my prodName with a value it has no problem, but obviously my user will not know the prodName ID and only the prod name. So when I try to insert the above I get the following error:
ValueError: Cannot assign "'SV1'": "ProcessedStockAmounts.prodName" must be a "Productlist" instance.
This was the closest I got to an answer and when I do the following it actually inserts:
p = ProcessedStockAmounts(amount='33', prodName = Productlist.objects.get(productid = 'SV1'), time = StockTakingTimes.objects.get(times='06:00'))
p.save()
but giving data this way is obviously defeating the purpose.
My serializer looks like the following:
class TestSerializer(serializers.ModelSerializer):
# time = serializers.SlugRelatedField(read_only=True, slug_field='time')
prodName = serializers.CharField()
# prodName = serializers.SlugRelatedField(read_only=True, slug_field='prodName')
class Meta:
model = ProcessedStockAmounts
fields = ('prodName','amount','time')
With my view:
class InsertMultiProcessedStock(APIView):
def post(self, request, format='json'):
serializer = TestSerializer(data = request.data, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
Productlist model:
class Productlist(models.Model):
productid = models.CharField(unique=True, max_length=20) # Field name made lowercase.
proddescription = models.CharField(db_column='prodDescription', max_length=255, blank=True, null=True) # Field name made lowercase.
packaging = models.ForeignKey(Packaging, on_delete=models.CASCADE, blank=True, null=True)
unitweight = models.FloatField(db_column='unitWeight', blank=True, null=True)
def __str__(self):
return self.productid
This would have been easier if you had the related model. But the commented-out slugrelatedfield is the way you should do it, using the actual field name:
prodName = serializers.SlugRelatedField(read_only=False, slug_field='productid')
Your serializer is wrong, You must use relationship serializer.
prodName = ProductlistSerializer(many = False)
But I found Your model defintion is very confusing