I want to show a form within another form.
For example I have these models:
class Measure(models.Model):
length = models.ForeignKey(Statistics, related_name='Length', null=True, blank=True)
surface_area = models.ForeignKey(Statistics, related_name='Surface area+', null=True, blank=True)
section_area = models.ForeignKey(Statistics, related_name='Section area+', null=True, blank=True)
volume = models.ForeignKey(Statistics, related_name='Volume', null=True, blank=True)
diameter = models.ForeignKey(Statistics, related_name='Diameter', null=True, blank=True)
class Statistics(models.Model):
total = models.FloatField('Total', null=True, blank=True)
avg = models.FloatField('Average', null=True, blank=True)
min = models.FloatField('Minimum', null=True, blank=True)
max = models.FloatField('Maximum', null=True, blank=True)
standard_deviation = models.FloatField('Standard deviation', null=True, blank=True)
and then I have these forms corresponding to the previous models:
class StatisticsForm(forms.ModelForm):
class Meta:
model = Statistics
fields = '__all__'
class MeasureForm(forms.ModelForm):
class Meta:
model = Measure
fields = '__all__'
# Here I have to say that the right form for each field is the StatisticForm
The ForeignKey in the forms is rendered as a combo box includes all the objects in the other table (in my case the Statistics table), I want to replace the combo box with an object of StatisticsForm so I can control the way I render the Statistics objects
Thank you very much.
Your database scheme and models are incorrectly designed to solve the problem at hand. You are defining a "has many" relationship in the wrong direction. One Measurement is supposed to have several Statistics, however one Statistics is not supposed to have many Measurement.
As your models are set up right now the ForeignKey is on the wrong side of the relationship. You should do this instead:
class Measure(models.Model):
def save(self,*args,**kwargs):
result = super(Measure, self).save(*args,**kwargs)
Statistics.objects.create(name='length', measurement=self)
Statistics.objects.create(name='section', measurement=self)
Statistics.objects.create(name='surface', measurement=self)
Statistics.objects.create(name='volume', measurement=self)
Statistics.objects.create(name='diameter', measurement=self)
return result
To provide the same comfort in accessing the Statisticss for one Measurement as in your current code you can add a couple of #property shortcuts:
class Measure(models.Model):
#property
def length(self):
return self.statistics_set.get(name='length')
#property
def section(self):
return self.statistics_set.get(name='section')
#property
def surface(self):
return self.statistics_set.get(name='surface')
#property
def volume(self):
return self.statistics_set.get(name='volume')
#property
def diameter(self):
return self.statistics_set.get(name='diameter')
class Statistics(models.Model):
name = models.CharField(max_length=10)
measurement = models.ForeignKey('Measurement')
total = models.FloatField('Total', null=True, blank=True)
avg = models.FloatField('Average', null=True, blank=True)
min = models.FloatField('Minimum', null=True, blank=True)
max = models.FloatField('Maximum', null=True, blank=True)
standard_deviation = models.FloatField('Standard deviation', null=True, blank=True)
Once you fix the relationship between the objects the problem becomes much easier to solve. Instead of being ForeignKeyFields in the form, the Statisticss become proper related objects, which are routinely handled by django.
As #solarisssmoke mentioned in the comments you are looking for formsets. Here is an example from the django documentation showing how to achieve what you need:
The models in question:
class Author(models.Model):
name = models.CharField(max_length=100)
title = models.CharField(max_length=3, choices=TITLE_CHOICES)
birth_date = models.DateField(blank=True, null=True)
def __str__(self): # __unicode__ on Python 2
return self.name
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
And a view using inlineformset_factory to create the needed formset:
from django.forms import inlineformset_factory
def manage_books(request, author_id):
author = Author.objects.get(pk=author_id)
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
if request.method == "POST":
formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
if formset.is_valid():
formset.save()
# Do something. Should generally end with a redirect. For example:
return HttpResponseRedirect(author.get_absolute_url())
else:
formset = BookInlineFormSet(instance=author)
return render(request, 'manage_books.html', {'formset': formset})
If performance becomes an issue also have a look at prefetch_related for boosting performance.
Related
I have the Account model were I store information about preferred units.
However I also want to allow user to change the units for particular exercise which by default should be Account.units.
Here are my models:
class Account(models.Model):
"""Model to store user's data and preferences."""
UNIT_CHOICES = [
('metric', 'Metric'),
('imperial', 'Imperial')
]
uuid = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=False)
units = models.CharField(max_length=255, choices=UNIT_CHOICES, default=UNIT_CHOICES[0], null=False, blank=False)
weight_metric = models.FloatField(null=True, blank=True)
height_metric = models.FloatField(null=True, blank=True)
weight_imperial = models.FloatField(null=True, blank=True)
height_imperial = models.FloatField(null=True, blank=True)
def __str__(self):
return self.owner.email
class CustomExercise(models.Model):
UNIT_CHOICES = [
('metric', 'Metric'),
('imperial', 'Imperial')
]
uuid = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(Account, on_delete=models.CASCADE, null=False, blank=False)
preferred_units = models.CharField(max_length=255, choices=UNIT_CHOICES, default=owner.units, null=False, blank=False) # <- throws an error that "ForeignKey doesn't have units attribute."
name = models.CharField(max_length=255, null=False, blank=False)
measure_time = models.BooleanField(default=False)
measure_distance = models.BooleanField(default=False)
measure_weight = models.BooleanField(default=False)
measure_reps = models.BooleanField(default=False)
def __str__(self):
return f'{self.owner}:{self.name}'
As posted in code sample I tried to get that default value from ForeignKey, which not unexpectedly did not work out.
So my question is: what is the correct solution to implement this kind of feature?
I would not recommend storing duplicate values accross multiple models. You can easily access that value through a property method:
class CustomExercise(models.Model):
... # remove preferred_units field from model
#property
def preferred_units(self):
return self.owner.unit
Although you can not use it in queryset directly, still you can annotate the 'owner__unit' field in queryset or filter by it:
q = CustomExcercise.objects.annotate(preferred_units=F('owner__unit')).filter(preferred_units = 'kg')
q.values()
Displaying the value in Adminsite:
class CustomExerciseAdmin(admin.ModelAdmin):
fields = (..., 'preferred_units')
readonly_fields = ['preferred_units']
Two ways come to mind: overriding the model's save method or by using a pre_save signal. I would try the first one and if it doesn't work then the second one. The reason is that signals are notoriously difficult to debug so if you have alternatives you should always leave them as a last resort.
Ok so, I think this should work:
def save(self, *args, **kwargs):
self.preferred_units = self.owner.units
super(CustomExercise, self).save(*args, **kwargs
Otherwise:
#receiver(pre_save, sender=CustomExercise)
def assign_unit(sender, instance, **kwargs):
instance.preferred_units = instance.owner.units
The convention is to store your signals in signals.py in your app. Make sure to "activate" them from apps.py or they won't work. Here the docs.
I have two models named Account and Story. With the help of a models.Manager on Django, I wrote a method that retrieves the stories posted in the last 24 hours. With Django, this method works very well. The problem is that when I create a serializer with Django Rest Framework it pulls all the stories. To prevent this, I wrote a method called get_image_file and used the method I wrote with the help of models.Manager here, but I could not solve the problem. As a result, how do I pull only the stories that were posted in the last 24 with Django Rest Framework, as I did on the Django side?
serializers.py
from rest_framework import serializers
from ..models import Account, Story
class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = ['image_file']
def get_image_file(self, obj):
#This method doesn't work. DRF pulls all the stories.
return obj.get_available_stories()
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = '__all__'
stories = StorySerializer(many=True)
models.py
from django.db import models
import datetime
def upload_to(instance, filename):
username = instance.account.username
return '%s/%s/%s' %('stories',username,filename)
def upload_pp(instance, filename):
return '%s/%s/%s' %('profile_photo', instance.username,filename)
class Account(models.Model):
username_pk = models.CharField(max_length=122)
username = models.CharField(max_length=55, unique=True)
profile_photo_id = models.CharField(max_length=155, unique=True)
profile_photo = models.ImageField(upload_to=upload_pp, null=True, blank=True)
profile_photo_width = models.IntegerField(null=True, blank=True)
profile_photo_height = models.IntegerField(null=True, blank=True)
def __str__(self):
return self.username
class StoryManager(models.Manager):
def get_available_stories(self, *args, **kwargs):
date_from = datetime.datetime.now() - datetime.timedelta(days=1)
return super(StoryManager, self).filter(image_timestamp__gte = date_from)
class Story(models.Model):
account = models.ForeignKey(to=Account, on_delete=models.CASCADE, related_name="stories")
image_file = models.ImageField(upload_to=upload_to, blank=True, null=True, editable=False)
image_url = models.URLField()
image_pk = models.CharField(max_length=60, null=True, blank=True, editable=False, unique=True)
image_timestamp = models.DateTimeField(null=True, blank=True)
image_width = models.IntegerField(null=True, blank=True)
image_height = models.IntegerField(null=True, blank=True)
objects = StoryManager()
def __str__(self):
return str(self.account)
An way to do that is using Filters Class.
Works very similar as Serializer class.
Custom Generic Filter
def filter_queryset(self, request, queryset, view):
# Override this method to do your filtering.
# Probably your BaseModel has some filed as created_at
# Assuming that is a Datetime object
return queryset.filter(created_at.hour__lte=24)
# You can even sort this values adding a .sort_by() after the filter
I want to serializing ManyToMany fields. So, in the response for tje Application model should also the band and the band description listed. But I've got the following error message:
Got AttributeError when attempting to get a value for field bands on
serializer OsdSerializer. The serializer field might be named
incorrectly and not match any attribute or key on the Application
instance. Original exception text was: 'Application' object has no
attribute 'bands'.
Anyone who knows why it doesnt work?
models.py
class Band(models.Model):
"""Database models for satellit's bands information"""
band = models.CharField(max_length=3)
description = models.CharField(max_length=255)
in_satellite = models.ForeignKey('Satellite', on_delete=models.PROTECT, blank=True, null=True)
wavelength = models.DecimalField(max_digits=4, decimal_places=0, default='0', help_text='Central wavelength (nm)')
resolution = models.DecimalField(max_digits=4, decimal_places=0, default='0', help_text='Spatial resolution (m)')
def __str__(self):
return '%s | %s' % (self.in_satellite, self.band)
class Satellite(models.Model):
"""Database models for satellite information"""
name = models.CharField(max_length=255)
accr = models.CharField(max_length=3)
operator = models.CharField(max_length=255, default='European Space Agency')
def __str__(self):
return self.name
class Indice(models.Model):
"""Database models for index information"""
name = models.CharField(max_length=255)
accr = models.CharField(max_length=10)
description = models.TextField(max_length=255, blank=True, null=True)
satellite_to_use = models.ForeignKey('Satellite', on_delete=models.PROTECT, blank=True, null=True)
needed_bands = models.ManyToManyField(Band)
def __str__(self):
return self.name
class Application(models.Model):
"""Database models for application information"""
name = models.CharField(max_length=255)
description = models.TextField(max_length=255, blank=True, null=True)
indice_to_use = models.ForeignKey('Indice', on_delete=models.PROTECT, blank=True, null=True)
def __str__(self):
return self.name
serializers.py
class BandSerializer(serializers.ModelSerializer):
class Meta:
model = Band
fields = ['band', 'description', ]
class OsdSerializer(serializers.ModelSerializer):
bands = BandSerializer()
class Meta:
model = Application
fields = ['name', 'description', 'bands',]
views.py
class OsdView(APIView):
def get(self, request):
applications = Application.objects.all()
serializer = OsdSerializer(applications, many=True)
return Response({"Your open space data:": serializer.data})
IIUC, you need to change the OsdSerializer to specify the relation you want to include in the serialized data:
bands = BandSerializer(source='indice_to_use.needed_bands', many=True)
Your code is correct but here i don't see any relation between two of your models Band and Application.
I think you might need to find some relation between Band model and Application model only then and then you will get the band object in output associated with corresponding application object.
Trying to create a column in my model called, stock_count, that finds the sum of the total string objects in my ArrayField(), aka stock_list. Here is my function.
def total_stocks_calc(self):
self.stock_count = Bucket.objects.aggregate(Sum('stock_list', distinct=True))
self.save()
However it doesn't seem to be doing anything, no calculating, leaving the field blank in my model, admin page, and DRF interface...
EDIT: updated post with new implementation.
Here is my model.
class Bucket(models.Model):
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=category_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,null=True),size=30,null=True)
about = models.CharField(max_length=75)
objects = models.Manager()
bucketobjects = BucketObjects()
class Meta:
ordering = ('-created',)
def total_stocks_calc(self):
self.stock_count = Bucket.objects.annotate(stock_count=F('stock_list__len'))
self.save()
def __unicode__(self):
return self.stock_list
Would like to know the proper way to count total items in ArrayField(), thank you in advance.
The ArrayField provides the len lookup field, through that you can get the count
like
from django.db.models import F
Bucket.objects.annotate(stock_count=F('stock_list__len'))
I have a many to many field ConnectedTo in my model and I want to create the object using a form. However when I list it as a field I just get a listbox with options to highlight and no way of selecting one or more.
Ideally I'd love a multiple selection checkbox with a list of items in a scroll box. But I'd start with just having a selectable item.
Here's my code so far:
models.py:
class Part(models.Model):
PartID = models.AutoField(primary_key=True, unique=True)
SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True)
Comment = models.CharField(max_length=255, blank=True)
Subtype = models.ForeignKey('Subtype', on_delete=models.CASCADE, null=True)
Location = models.CharField(max_length=255, blank=True)
ConnectedTo= models.ManyToManyField('self', blank=True, null=True)
BatchNo = models.CharField(max_length=32, blank=False, null=True)
SerialNo = models.CharField(max_length=32,blank=True)
Manufacturer = models.CharField(max_length=32, blank=False, null=True)
Length = models.CharField(max_length=6, blank=True, null=True)
InspectionPeriod = models.IntegerField(blank=True, null=True)
LastInspected = models.DateField(blank=True, null=True)
InspectionDue = models.CharField(max_length=255, blank=True)
#classmethod
def create(cls, siteid, comment, subtype, location, batchno, serialno, manufacturer, length, inspectionperiod, lastinspected, inspectiondue):
part = cls(SiteID = siteid, Comment = comment, Subtype = subtype, Location = location, BatchNo = batchno, SerialNo = serialno, Manufacturer = manufacturer, Length = length, InspectionPeriod = inspectionperiod, LastInspected = lastinspected, InspectionDue = inspectiondue)
return part
def __str__(self):
return str(self.PartID)
forms.py:
class PartForm(forms.ModelForm):
class Meta:
model = Part
fields = ('Comment', 'Subtype', 'Location', 'ConnectedTo', 'BatchNo', 'SerialNo', 'Manufacturer', 'Length', 'InspectionPeriod', 'LastInspected')
views.py:
#login_required(login_url='/accounts/login/')
def addPartForm_Create(request, site, subtype):
siteselected = site
subtypeselected = Subtype.objects.get(SubtypeID = subtype)
if request.method == 'POST':
form = addPartForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.SiteID = Site.objects.get(SiteID = siteselected)
obj.Subtype = subtypeselected
obj.save()
return redirect('/sites/'+str(site))
else:
form = addPartForm()
return render(request, 'myproj/addPart.html', {'form': form, 'SiteNo': Site.objects.get(SiteID = siteselected).SiteID, 'subtype': subtypeselected})
EDIT: had the wrong view, sorry.
EDIT 2: example of what I mean by the highlighted box:
UPDATE:
Jey_Jen's answer has helped me get the style I want. I now have a multiple selection checkbox. But the ConnectedTo attributes do not save. Everything else in the model is saved and a new part is created. But no many to many links.
I would suggest looking into django form widgets. you can override the default widget to be a whatever you want. you can view them here.
heres a small example the django docs give:
class CommentForm(forms.Form):
name = forms.CharField()
url = forms.URLField()
comment = forms.CharField(widget=forms.Textarea)